From c7fdde81cd48f1ba5fa60b4bca10335e32e4bcee Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 30 Jun 2020 08:21:49 +0200 Subject: [PATCH] add MailKit and MimeKit --- src/MailKit/AccessControl.cs | 137 + src/MailKit/AccessControlList.cs | 66 + src/MailKit/AccessRight.cs | 232 + src/MailKit/AccessRights.cs | 317 + src/MailKit/AlertEventArgs.cs | 69 + src/MailKit/Annotation.cs | 81 + src/MailKit/AnnotationAccess.cs | 53 + src/MailKit/AnnotationAttribute.cs | 251 + src/MailKit/AnnotationEntry.cs | 521 + src/MailKit/AnnotationScope.cs | 61 + src/MailKit/AnnotationsChangedEventArgs.cs | 93 + src/MailKit/AuthenticatedEventArgs.cs | 68 + src/MailKit/BodyPart.cs | 716 + src/MailKit/BodyPartBasic.cs | 245 + src/MailKit/BodyPartCollection.cs | 276 + src/MailKit/BodyPartMessage.cs | 124 + src/MailKit/BodyPartMultipart.cs | 141 + src/MailKit/BodyPartText.cs | 123 + src/MailKit/BodyPartVisitor.cs | 137 + src/MailKit/CommandException.cs | 102 + src/MailKit/CompressedStream.cs | 435 + src/MailKit/ConnectedEventArgs.cs | 88 + src/MailKit/DeliveryStatusNotification.cs | 61 + src/MailKit/DeliveryStatusNotificationType.cs | 54 + src/MailKit/DisconnectedEventArgs.cs | 70 + src/MailKit/DuplexStream.cs | 395 + src/MailKit/Envelope.cs | 592 + src/MailKit/FolderAccess.cs | 53 + src/MailKit/FolderAttributes.cs | 135 + src/MailKit/FolderCreatedEventArgs.cs | 67 + src/MailKit/FolderFeature.cs | 82 + src/MailKit/FolderNamespace.cs | 74 + src/MailKit/FolderNamespaceCollection.cs | 235 + src/MailKit/FolderNotFoundException.cs | 149 + src/MailKit/FolderNotOpenException.cs | 184 + src/MailKit/FolderQuota.cs | 124 + src/MailKit/FolderRenamedEventArgs.cs | 85 + src/MailKit/IMailFolder.cs | 5077 +++++ src/MailKit/IMailService.cs | 1118 ++ src/MailKit/IMailSpool.cs | 511 + src/MailKit/IMailStore.cs | 549 + src/MailKit/IMailTransport.cs | 179 + src/MailKit/IMessageSummary.cs | 460 + src/MailKit/IProtocolLogger.cs | 116 + src/MailKit/ITransferProgress.cs | 58 + src/MailKit/MailFolder.cs | 16501 ++++++++++++++++ src/MailKit/MailService.cs | 1582 ++ src/MailKit/MailSpool.cs | 1588 ++ src/MailKit/MailStore.cs | 872 + src/MailKit/MailTransport.cs | 507 + src/MailKit/MessageEventArgs.cs | 98 + src/MailKit/MessageFlags.cs | 78 + src/MailKit/MessageFlagsChangedEventArgs.cs | 269 + src/MailKit/MessageLabelsChangedEventArgs.cs | 170 + src/MailKit/MessageNotFoundException.cs | 92 + src/MailKit/MessageSentEventArgs.cs | 87 + src/MailKit/MessageSorter.cs | 295 + src/MailKit/MessageSummary.cs | 758 + src/MailKit/MessageSummaryFetchedEventArgs.cs | 67 + src/MailKit/MessageSummaryItems.cs | 222 + src/MailKit/MessageThread.cs | 108 + src/MailKit/MessageThreader.cs | 603 + src/MailKit/MessagesVanishedEventArgs.cs | 79 + src/MailKit/Metadata.cs | 74 + src/MailKit/MetadataChangedEventArgs.cs | 67 + src/MailKit/MetadataCollection.cs | 59 + src/MailKit/MetadataOptions.cs | 108 + src/MailKit/MetadataTag.cs | 156 + src/MailKit/ModSeqChangedEventArgs.cs | 86 + src/MailKit/Net/Imap/AsyncImapClient.cs | 965 + src/MailKit/Net/Imap/IImapClient.cs | 628 + src/MailKit/Net/Imap/IImapFolder.cs | 869 + src/MailKit/Net/Imap/ImapCallbacks.cs | 68 + src/MailKit/Net/Imap/ImapCapabilities.cs | 348 + src/MailKit/Net/Imap/ImapClient.cs | 2616 +++ src/MailKit/Net/Imap/ImapCommand.cs | 918 + src/MailKit/Net/Imap/ImapCommandException.cs | 190 + src/MailKit/Net/Imap/ImapCommandResponse.cs | 55 + src/MailKit/Net/Imap/ImapEncoding.cs | 155 + src/MailKit/Net/Imap/ImapEngine.cs | 2905 +++ src/MailKit/Net/Imap/ImapEventGroup.cs | 692 + src/MailKit/Net/Imap/ImapFolder.cs | 6224 ++++++ src/MailKit/Net/Imap/ImapFolderAnnotations.cs | 567 + .../Net/Imap/ImapFolderConstructorArgs.cs | 113 + src/MailKit/Net/Imap/ImapFolderFetch.cs | 6770 +++++++ src/MailKit/Net/Imap/ImapFolderFlags.cs | 2888 +++ src/MailKit/Net/Imap/ImapFolderSearch.cs | 1773 ++ src/MailKit/Net/Imap/ImapImplementation.cs | 217 + src/MailKit/Net/Imap/ImapProtocolException.cs | 109 + src/MailKit/Net/Imap/ImapResponseCode.cs | 373 + .../Net/Imap/ImapSearchQueryOptimizer.cs | 83 + src/MailKit/Net/Imap/ImapStream.cs | 1190 ++ src/MailKit/Net/Imap/ImapToken.cs | 80 + src/MailKit/Net/Imap/ImapUtils.cs | 1670 ++ src/MailKit/Net/NetworkStream.cs | 298 + src/MailKit/Net/Pop3/AsyncPop3Client.cs | 1496 ++ src/MailKit/Net/Pop3/IPop3Client.cs | 309 + src/MailKit/Net/Pop3/Pop3Capabilities.cs | 129 + src/MailKit/Net/Pop3/Pop3Client.cs | 3401 ++++ src/MailKit/Net/Pop3/Pop3Command.cs | 74 + src/MailKit/Net/Pop3/Pop3CommandException.cs | 179 + src/MailKit/Net/Pop3/Pop3Engine.cs | 655 + src/MailKit/Net/Pop3/Pop3Language.cs | 71 + src/MailKit/Net/Pop3/Pop3ProtocolException.cs | 101 + src/MailKit/Net/Pop3/Pop3Stream.cs | 950 + src/MailKit/Net/Proxy/HttpProxyClient.cs | 248 + src/MailKit/Net/Proxy/IProxyClient.cs | 208 + src/MailKit/Net/Proxy/ProxyClient.cs | 406 + .../Net/Proxy/ProxyProtocolException.cs | 97 + src/MailKit/Net/Proxy/Socks4Client.cs | 299 + src/MailKit/Net/Proxy/Socks4aClient.cs | 88 + src/MailKit/Net/Proxy/Socks5Client.cs | 421 + src/MailKit/Net/Proxy/SocksClient.cs | 100 + src/MailKit/Net/SelectMode.cs | 36 + src/MailKit/Net/Smtp/AsyncSmtpClient.cs | 689 + src/MailKit/Net/Smtp/ISmtpClient.cs | 246 + src/MailKit/Net/Smtp/SmtpCapabilities.cs | 106 + src/MailKit/Net/Smtp/SmtpClient.cs | 2465 +++ src/MailKit/Net/Smtp/SmtpCommandException.cs | 258 + src/MailKit/Net/Smtp/SmtpDataFilter.cs | 140 + src/MailKit/Net/Smtp/SmtpProtocolException.cs | 101 + src/MailKit/Net/Smtp/SmtpResponse.cs | 68 + src/MailKit/Net/Smtp/SmtpStatusCode.cs | 190 + src/MailKit/Net/Smtp/SmtpStream.cs | 903 + src/MailKit/Net/SocketUtils.cs | 118 + src/MailKit/Net/SslStream.cs | 44 + src/MailKit/NullProtocolLogger.cs | 113 + src/MailKit/ProgressStream.cs | 185 + src/MailKit/Properties/AssemblyInfo.cs | 84 + src/MailKit/ProtocolException.cs | 100 + src/MailKit/ProtocolLogger.cs | 341 + src/MailKit/Search/AnnotationSearchQuery.cs | 105 + src/MailKit/Search/BinarySearchQuery.cs | 100 + src/MailKit/Search/DateSearchQuery.cs | 62 + src/MailKit/Search/FilterSearchQuery.cs | 94 + src/MailKit/Search/HeaderSearchQuery.cs | 91 + src/MailKit/Search/ISearchQueryOptimizer.cs | 32 + src/MailKit/Search/NumericSearchQuery.cs | 60 + src/MailKit/Search/OrderBy.cs | 223 + src/MailKit/Search/OrderByAnnotation.cs | 91 + src/MailKit/Search/OrderByType.cs | 90 + src/MailKit/Search/SearchOptions.cs | 69 + src/MailKit/Search/SearchQuery.cs | 1136 ++ src/MailKit/Search/SearchResults.cs | 116 + src/MailKit/Search/SearchTerm.cs | 288 + src/MailKit/Search/SortOrder.cs | 50 + src/MailKit/Search/TextSearchQuery.cs | 75 + src/MailKit/Search/UidSearchQuery.cs | 94 + src/MailKit/Search/UnarySearchQuery.cs | 82 + .../Security/AuthenticationException.cs | 94 + src/MailKit/Security/KeyedHashAlgorithm.cs | 137 + src/MailKit/Security/Ntlm/BitConverterLE.cs | 112 + .../Security/Ntlm/ChallengeResponse2.cs | 280 + src/MailKit/Security/Ntlm/DES.cs | 232 + src/MailKit/Security/Ntlm/HMACMD5.cs | 202 + src/MailKit/Security/Ntlm/MD4.cs | 410 + src/MailKit/Security/Ntlm/MessageBase.cs | 99 + src/MailKit/Security/Ntlm/NtlmAuthLevel.cs | 51 + src/MailKit/Security/Ntlm/NtlmFlags.cs | 222 + src/MailKit/Security/Ntlm/TargetInfo.cs | 261 + src/MailKit/Security/Ntlm/Type1Message.cs | 167 + src/MailKit/Security/Ntlm/Type2Message.cs | 191 + src/MailKit/Security/Ntlm/Type3Message.cs | 284 + src/MailKit/Security/RandomNumberGenerator.cs | 52 + src/MailKit/Security/SaslException.cs | 158 + src/MailKit/Security/SaslMechanism.cs | 602 + src/MailKit/Security/SaslMechanismCramMd5.cs | 215 + .../Security/SaslMechanismDigestMd5.cs | 571 + src/MailKit/Security/SaslMechanismLogin.cs | 297 + src/MailKit/Security/SaslMechanismNtlm.cs | 230 + src/MailKit/Security/SaslMechanismOAuth2.cs | 179 + src/MailKit/Security/SaslMechanismPlain.cs | 293 + .../Security/SaslMechanismScramBase.cs | 376 + .../Security/SaslMechanismScramSha1.cs | 151 + .../Security/SaslMechanismScramSha256.cs | 151 + src/MailKit/Security/SecureSocketOptions.cs | 68 + src/MailKit/Security/SslHandshakeException.cs | 305 + .../ServiceNotAuthenticatedException.cs | 97 + src/MailKit/ServiceNotConnectedException.cs | 97 + src/MailKit/SpecialFolder.cs | 75 + src/MailKit/StatusItems.cs | 88 + src/MailKit/ThreadingAlgorithm.cs | 48 + src/MailKit/UniqueId.cs | 428 + src/MailKit/UniqueIdMap.cs | 223 + src/MailKit/UniqueIdRange.cs | 494 + src/MailKit/UniqueIdSet.cs | 988 + src/MailKit/UriExtensions.cs | 70 + src/MailKit/mailkit.snk | Bin 0 -> 596 bytes src/MailKitMailModule.cs | 14 + src/MimeKit/AsyncMimeParser.cs | 706 + src/MimeKit/AttachmentCollection.cs | 653 + src/MimeKit/BodyBuilder.cs | 186 + src/MimeKit/CancellationToken.cs | 76 + src/MimeKit/ContentDisposition.cs | 916 + src/MimeKit/ContentEncoding.cs | 83 + src/MimeKit/ContentType.cs | 881 + .../Cryptography/ApplicationPgpEncrypted.cs | 97 + .../Cryptography/ApplicationPgpSignature.cs | 106 + .../Cryptography/ApplicationPkcs7Mime.cs | 924 + .../Cryptography/ApplicationPkcs7Signature.cs | 105 + src/MimeKit/Cryptography/ArcSigner.cs | 729 + src/MimeKit/Cryptography/ArcVerifier.cs | 694 + .../AsymmetricAlgorithmExtensions.cs | 373 + .../Cryptography/AuthenticationResults.cs | 1366 ++ .../BouncyCastleCertificateExtensions.cs | 365 + .../BouncyCastleSecureMimeContext.cs | 1339 ++ .../CertificateNotFoundException.cs | 115 + src/MimeKit/Cryptography/CmsRecipient.cs | 256 + .../Cryptography/CmsRecipientCollection.cs | 208 + src/MimeKit/Cryptography/CmsSigner.cs | 464 + .../Cryptography/CryptographyContext.cs | 648 + src/MimeKit/Cryptography/DbExtensions.cs | 50 + .../Cryptography/DefaultSecureMimeContext.cs | 668 + src/MimeKit/Cryptography/DigestAlgorithm.cs | 112 + .../DigitalSignatureCollection.cs | 54 + .../DigitalSignatureVerifyException.cs | 147 + src/MimeKit/Cryptography/DkimBodyFilter.cs | 72 + .../DkimCanonicalizationAlgorithm.cs | 59 + src/MimeKit/Cryptography/DkimHashStream.cs | 370 + .../Cryptography/DkimPublicKeyLocatorBase.cs | 189 + .../Cryptography/DkimRelaxedBodyFilter.cs | 167 + .../Cryptography/DkimSignatureAlgorithm.cs | 53 + .../Cryptography/DkimSignatureStream.cs | 361 + src/MimeKit/Cryptography/DkimSigner.cs | 483 + src/MimeKit/Cryptography/DkimSignerBase.cs | 293 + .../Cryptography/DkimSimpleBodyFilter.cs | 140 + src/MimeKit/Cryptography/DkimVerifier.cs | 308 + src/MimeKit/Cryptography/DkimVerifierBase.cs | 495 + .../Cryptography/Ed25519DigestSigner.cs | 112 + .../Cryptography/EncryptionAlgorithm.cs | 142 + src/MimeKit/Cryptography/GnuPGContext.cs | 268 + .../Cryptography/IDigitalCertificate.cs | 92 + src/MimeKit/Cryptography/IDigitalSignature.cs | 99 + .../Cryptography/IDkimPublicKeyLocator.cs | 96 + .../Cryptography/IX509CertificateDatabase.cs | 196 + src/MimeKit/Cryptography/LdapUri.cs | 161 + src/MimeKit/Cryptography/MD5.cs | 749 + .../Cryptography/MacSecureMimeContext.cs | 267 + .../Cryptography/MultipartEncrypted.cs | 1207 ++ src/MimeKit/Cryptography/MultipartSigned.cs | 581 + .../Cryptography/NpgsqlCertificateDatabase.cs | 264 + .../Cryptography/OpenPgpBlockFilter.cs | 208 + src/MimeKit/Cryptography/OpenPgpContext.cs | 1185 ++ .../Cryptography/OpenPgpContextBase.cs | 1887 ++ src/MimeKit/Cryptography/OpenPgpDataType.cs | 62 + .../Cryptography/OpenPgpDetectionFilter.cs | 344 + .../Cryptography/OpenPgpDigitalCertificate.cs | 184 + .../Cryptography/OpenPgpDigitalSignature.cs | 164 + .../Cryptography/OpenPgpKeyCertification.cs | 65 + .../PrivateKeyNotFoundException.cs | 152 + .../Cryptography/PublicKeyAlgorithm.cs | 90 + .../PublicKeyNotFoundException.cs | 115 + .../Cryptography/RsaEncryptionPadding.cs | 251 + .../RsaEncryptionPaddingScheme.cs | 47 + .../Cryptography/RsaSignaturePadding.cs | 153 + .../Cryptography/RsaSignaturePaddingScheme.cs | 47 + .../Cryptography/SecureMailboxAddress.cs | 219 + src/MimeKit/Cryptography/SecureMimeContext.cs | 842 + .../SecureMimeDigitalCertificate.cs | 149 + .../SecureMimeDigitalSignature.cs | 265 + src/MimeKit/Cryptography/SecureMimeType.cs | 65 + .../Cryptography/SqlCertificateDatabase.cs | 832 + .../Cryptography/SqliteCertificateDatabase.cs | 394 + .../Cryptography/SubjectIdentifierType.cs | 50 + .../TemporarySecureMimeContext.cs | 462 + .../Cryptography/WindowsSecureMimeContext.cs | 1248 ++ .../WindowsSecureMimeDigitalCertificate.cs | 148 + .../WindowsSecureMimeDigitalSignature.cs | 212 + .../X509Certificate2Extensions.cs | 153 + .../Cryptography/X509CertificateChain.cs | 381 + .../Cryptography/X509CertificateDatabase.cs | 985 + .../Cryptography/X509CertificateRecord.cs | 315 + .../Cryptography/X509CertificateStore.cs | 544 + src/MimeKit/Cryptography/X509CrlRecord.cs | 172 + src/MimeKit/Cryptography/X509KeyUsageFlags.cs | 121 + src/MimeKit/DomainList.cs | 477 + src/MimeKit/EncodingConstraint.cs | 52 + src/MimeKit/Encodings/Base64Decoder.cs | 251 + src/MimeKit/Encodings/Base64Encoder.cs | 337 + src/MimeKit/Encodings/HexDecoder.cs | 232 + src/MimeKit/Encodings/HexEncoder.cs | 216 + src/MimeKit/Encodings/IMimeDecoder.cs | 117 + src/MimeKit/Encodings/IMimeEncoder.cs | 132 + src/MimeKit/Encodings/PassThroughDecoder.cs | 172 + src/MimeKit/Encodings/PassThroughEncoder.cs | 178 + src/MimeKit/Encodings/QEncoder.cs | 260 + .../Encodings/QuotedPrintableDecoder.cs | 277 + .../Encodings/QuotedPrintableEncoder.cs | 317 + src/MimeKit/Encodings/UUDecoder.cs | 418 + src/MimeKit/Encodings/UUEncoder.cs | 375 + src/MimeKit/Encodings/YDecoder.cs | 544 + src/MimeKit/Encodings/YEncoder.cs | 272 + src/MimeKit/FormatOptions.cs | 366 + src/MimeKit/GroupAddress.cs | 747 + src/MimeKit/Header.cs | 1582 ++ src/MimeKit/HeaderId.cs | 792 + src/MimeKit/HeaderList.cs | 1547 ++ src/MimeKit/HeaderListChangedEventArgs.cs | 74 + src/MimeKit/HeaderListCollection.cs | 254 + src/MimeKit/IMimeContent.cs | 178 + src/MimeKit/IO/BoundStream.cs | 718 + src/MimeKit/IO/ChainedStream.cs | 700 + src/MimeKit/IO/FilteredStream.cs | 812 + src/MimeKit/IO/Filters/ArmoredFromFilter.cs | 167 + src/MimeKit/IO/Filters/BestEncodingFilter.cs | 273 + src/MimeKit/IO/Filters/CharsetFilter.cs | 229 + src/MimeKit/IO/Filters/DecoderFilter.cs | 159 + src/MimeKit/IO/Filters/Dos2UnixFilter.cs | 121 + src/MimeKit/IO/Filters/EncoderFilter.cs | 163 + src/MimeKit/IO/Filters/IMimeFilter.cs | 74 + src/MimeKit/IO/Filters/MimeFilterBase.cs | 235 + src/MimeKit/IO/Filters/PassThroughFilter.cs | 100 + .../IO/Filters/TrailingWhitespaceFilter.cs | 140 + src/MimeKit/IO/Filters/Unix2DosFilter.cs | 120 + src/MimeKit/IO/ICancellableStream.cs | 95 + src/MimeKit/IO/MeasuringStream.cs | 427 + src/MimeKit/IO/MemoryBlockStream.cs | 557 + src/MimeKit/InternetAddress.cs | 1361 ++ src/MimeKit/InternetAddressList.cs | 1110 ++ src/MimeKit/MacInterop/CFArray.cs | 56 + src/MimeKit/MacInterop/CFData.cs | 85 + src/MimeKit/MacInterop/CFDictionary.cs | 177 + src/MimeKit/MacInterop/CFObject.cs | 76 + src/MimeKit/MacInterop/CFRange.cs | 55 + src/MimeKit/MacInterop/CFString.cs | 131 + .../MacInterop/CssmDbAttributeFormat.cs | 41 + src/MimeKit/MacInterop/CssmKeyUse.cs | 43 + .../MacInterop/CssmTPAppleCertStatus.cs | 39 + src/MimeKit/MacInterop/Dlfcn.cs | 228 + src/MimeKit/MacInterop/OSStatus.cs | 40 + src/MimeKit/MacInterop/SecCertificate.cs | 69 + src/MimeKit/MacInterop/SecExternalFormat.cs | 57 + src/MimeKit/MacInterop/SecItemAttr.cs | 60 + src/MimeKit/MacInterop/SecItemClass.cs | 40 + src/MimeKit/MacInterop/SecItemExportFlags.cs | 34 + src/MimeKit/MacInterop/SecKeyAttribute.cs | 59 + src/MimeKit/MacInterop/SecKeychain.cs | 471 + .../MacInterop/SecKeychainAttribute.cs | 45 + .../MacInterop/SecKeychainAttributeList.cs | 43 + src/MimeKit/MailboxAddress.cs | 1098 + src/MimeKit/MessageDeliveryStatus.cs | 152 + src/MimeKit/MessageDispositionNotification.cs | 129 + src/MimeKit/MessageIdList.cs | 378 + src/MimeKit/MessageImportance.cs | 50 + src/MimeKit/MessagePart.cs | 288 + src/MimeKit/MessagePartial.cs | 511 + src/MimeKit/MessagePriority.cs | 50 + src/MimeKit/MimeContent.cs | 353 + src/MimeKit/MimeEntity.cs | 1732 ++ src/MimeKit/MimeEntityConstructorArgs.cs | 51 + src/MimeKit/MimeFormat.cs | 51 + src/MimeKit/MimeIterator.cs | 455 + src/MimeKit/MimeMessage.cs | 3185 +++ src/MimeKit/MimeParser.cs | 2028 ++ src/MimeKit/MimePart.cs | 761 + src/MimeKit/MimeTypes.cs | 1074 + src/MimeKit/MimeVisitor.cs | 385 + src/MimeKit/Multipart.cs | 828 + src/MimeKit/MultipartAlternative.cs | 184 + src/MimeKit/MultipartRelated.cs | 344 + src/MimeKit/MultipartReport.cs | 151 + src/MimeKit/Parameter.cs | 713 + src/MimeKit/ParameterEncodingMethod.cs | 58 + src/MimeKit/ParameterList.cs | 1092 + src/MimeKit/ParseException.cs | 146 + src/MimeKit/ParserOptions.cs | 405 + src/MimeKit/Properties/AssemblyInfo.cs | 83 + src/MimeKit/RfcComplianceMode.cs | 45 + src/MimeKit/StreamExtensions.cs | 46 + src/MimeKit/Text/CharBuffer.cs | 93 + src/MimeKit/Text/FlowedToHtml.cs | 423 + src/MimeKit/Text/FlowedToText.cs | 177 + src/MimeKit/Text/HeaderFooterFormat.cs | 45 + src/MimeKit/Text/HtmlAttribute.cs | 143 + src/MimeKit/Text/HtmlAttributeCollection.cs | 125 + src/MimeKit/Text/HtmlAttributeId.cs | 656 + src/MimeKit/Text/HtmlEntityDecoder.cs | 258 + src/MimeKit/Text/HtmlEntityDecoder.g.cs | 12445 ++++++++++++ src/MimeKit/Text/HtmlNamespace.cs | 133 + src/MimeKit/Text/HtmlTagCallback.cs | 42 + src/MimeKit/Text/HtmlTagContext.cs | 223 + src/MimeKit/Text/HtmlTagId.cs | 871 + src/MimeKit/Text/HtmlTextPreviewer.cs | 253 + src/MimeKit/Text/HtmlToHtml.cs | 357 + src/MimeKit/Text/HtmlToken.cs | 656 + src/MimeKit/Text/HtmlTokenKind.cs | 65 + src/MimeKit/Text/HtmlTokenizer.cs | 2937 +++ src/MimeKit/Text/HtmlTokenizerState.cs | 495 + src/MimeKit/Text/HtmlUtils.cs | 756 + src/MimeKit/Text/HtmlWriter.cs | 928 + src/MimeKit/Text/HtmlWriterState.cs | 53 + src/MimeKit/Text/ICharArray.cs | 77 + src/MimeKit/Text/PlainTextPreviewer.cs | 158 + src/MimeKit/Text/TextConverter.cs | 363 + src/MimeKit/Text/TextFormat.cs | 70 + src/MimeKit/Text/TextPreviewer.cs | 239 + src/MimeKit/Text/TextToFlowed.cs | 213 + src/MimeKit/Text/TextToHtml.cs | 352 + src/MimeKit/Text/TextToText.cs | 107 + src/MimeKit/Text/Trie.cs | 353 + src/MimeKit/Text/UrlScanner.cs | 618 + src/MimeKit/TextPart.cs | 538 + src/MimeKit/TextRfc822Headers.cs | 123 + src/MimeKit/Tnef/RtfCompressedToRtf.cs | 346 + src/MimeKit/Tnef/RtfCompressionMode.cs | 50 + src/MimeKit/Tnef/TnefAttachFlags.cs | 59 + src/MimeKit/Tnef/TnefAttachMethod.cs | 61 + src/MimeKit/Tnef/TnefAttributeLevel.cs | 45 + src/MimeKit/Tnef/TnefAttributeTag.cs | 213 + src/MimeKit/Tnef/TnefComplianceMode.cs | 46 + src/MimeKit/Tnef/TnefComplianceStatus.cs | 123 + src/MimeKit/Tnef/TnefException.cs | 126 + src/MimeKit/Tnef/TnefNameId.cs | 156 + src/MimeKit/Tnef/TnefNameIdKind.cs | 45 + src/MimeKit/Tnef/TnefPart.cs | 647 + src/MimeKit/Tnef/TnefPropertyId.cs | 2570 +++ src/MimeKit/Tnef/TnefPropertyReader.cs | 1658 ++ src/MimeKit/Tnef/TnefPropertyTag.cs | 5741 ++++++ src/MimeKit/Tnef/TnefPropertyType.cs | 125 + src/MimeKit/Tnef/TnefReader.cs | 758 + src/MimeKit/Tnef/TnefReaderStream.cs | 270 + src/MimeKit/Utils/BufferPool.cs | 180 + src/MimeKit/Utils/ByteExtensions.cs | 245 + src/MimeKit/Utils/CharsetUtils.cs | 590 + src/MimeKit/Utils/Crc32.cs | 179 + src/MimeKit/Utils/DateUtils.cs | 723 + src/MimeKit/Utils/MimeUtils.cs | 461 + src/MimeKit/Utils/OptimizedOrdinalComparer.cs | 140 + src/MimeKit/Utils/PackedByteArray.cs | 261 + src/MimeKit/Utils/ParseUtils.cs | 558 + src/MimeKit/Utils/Rfc2047.cs | 1566 ++ src/MimeKit/Utils/StringBuilderExtensions.cs | 158 + src/MimeKit/XMessagePriority.cs | 61 + src/MimeKit/mimekit.snk | Bin 0 -> 596 bytes 434 files changed, 211308 insertions(+) create mode 100644 src/MailKit/AccessControl.cs create mode 100644 src/MailKit/AccessControlList.cs create mode 100644 src/MailKit/AccessRight.cs create mode 100644 src/MailKit/AccessRights.cs create mode 100644 src/MailKit/AlertEventArgs.cs create mode 100644 src/MailKit/Annotation.cs create mode 100644 src/MailKit/AnnotationAccess.cs create mode 100644 src/MailKit/AnnotationAttribute.cs create mode 100644 src/MailKit/AnnotationEntry.cs create mode 100644 src/MailKit/AnnotationScope.cs create mode 100644 src/MailKit/AnnotationsChangedEventArgs.cs create mode 100644 src/MailKit/AuthenticatedEventArgs.cs create mode 100644 src/MailKit/BodyPart.cs create mode 100644 src/MailKit/BodyPartBasic.cs create mode 100644 src/MailKit/BodyPartCollection.cs create mode 100644 src/MailKit/BodyPartMessage.cs create mode 100644 src/MailKit/BodyPartMultipart.cs create mode 100644 src/MailKit/BodyPartText.cs create mode 100644 src/MailKit/BodyPartVisitor.cs create mode 100644 src/MailKit/CommandException.cs create mode 100644 src/MailKit/CompressedStream.cs create mode 100644 src/MailKit/ConnectedEventArgs.cs create mode 100644 src/MailKit/DeliveryStatusNotification.cs create mode 100644 src/MailKit/DeliveryStatusNotificationType.cs create mode 100644 src/MailKit/DisconnectedEventArgs.cs create mode 100644 src/MailKit/DuplexStream.cs create mode 100644 src/MailKit/Envelope.cs create mode 100644 src/MailKit/FolderAccess.cs create mode 100644 src/MailKit/FolderAttributes.cs create mode 100644 src/MailKit/FolderCreatedEventArgs.cs create mode 100644 src/MailKit/FolderFeature.cs create mode 100644 src/MailKit/FolderNamespace.cs create mode 100644 src/MailKit/FolderNamespaceCollection.cs create mode 100644 src/MailKit/FolderNotFoundException.cs create mode 100644 src/MailKit/FolderNotOpenException.cs create mode 100644 src/MailKit/FolderQuota.cs create mode 100644 src/MailKit/FolderRenamedEventArgs.cs create mode 100644 src/MailKit/IMailFolder.cs create mode 100644 src/MailKit/IMailService.cs create mode 100644 src/MailKit/IMailSpool.cs create mode 100644 src/MailKit/IMailStore.cs create mode 100644 src/MailKit/IMailTransport.cs create mode 100644 src/MailKit/IMessageSummary.cs create mode 100644 src/MailKit/IProtocolLogger.cs create mode 100644 src/MailKit/ITransferProgress.cs create mode 100644 src/MailKit/MailFolder.cs create mode 100644 src/MailKit/MailService.cs create mode 100644 src/MailKit/MailSpool.cs create mode 100644 src/MailKit/MailStore.cs create mode 100644 src/MailKit/MailTransport.cs create mode 100644 src/MailKit/MessageEventArgs.cs create mode 100644 src/MailKit/MessageFlags.cs create mode 100644 src/MailKit/MessageFlagsChangedEventArgs.cs create mode 100644 src/MailKit/MessageLabelsChangedEventArgs.cs create mode 100644 src/MailKit/MessageNotFoundException.cs create mode 100644 src/MailKit/MessageSentEventArgs.cs create mode 100644 src/MailKit/MessageSorter.cs create mode 100644 src/MailKit/MessageSummary.cs create mode 100644 src/MailKit/MessageSummaryFetchedEventArgs.cs create mode 100644 src/MailKit/MessageSummaryItems.cs create mode 100644 src/MailKit/MessageThread.cs create mode 100644 src/MailKit/MessageThreader.cs create mode 100644 src/MailKit/MessagesVanishedEventArgs.cs create mode 100644 src/MailKit/Metadata.cs create mode 100644 src/MailKit/MetadataChangedEventArgs.cs create mode 100644 src/MailKit/MetadataCollection.cs create mode 100644 src/MailKit/MetadataOptions.cs create mode 100644 src/MailKit/MetadataTag.cs create mode 100644 src/MailKit/ModSeqChangedEventArgs.cs create mode 100644 src/MailKit/Net/Imap/AsyncImapClient.cs create mode 100644 src/MailKit/Net/Imap/IImapClient.cs create mode 100644 src/MailKit/Net/Imap/IImapFolder.cs create mode 100644 src/MailKit/Net/Imap/ImapCallbacks.cs create mode 100644 src/MailKit/Net/Imap/ImapCapabilities.cs create mode 100644 src/MailKit/Net/Imap/ImapClient.cs create mode 100644 src/MailKit/Net/Imap/ImapCommand.cs create mode 100644 src/MailKit/Net/Imap/ImapCommandException.cs create mode 100644 src/MailKit/Net/Imap/ImapCommandResponse.cs create mode 100644 src/MailKit/Net/Imap/ImapEncoding.cs create mode 100644 src/MailKit/Net/Imap/ImapEngine.cs create mode 100644 src/MailKit/Net/Imap/ImapEventGroup.cs create mode 100644 src/MailKit/Net/Imap/ImapFolder.cs create mode 100644 src/MailKit/Net/Imap/ImapFolderAnnotations.cs create mode 100644 src/MailKit/Net/Imap/ImapFolderConstructorArgs.cs create mode 100644 src/MailKit/Net/Imap/ImapFolderFetch.cs create mode 100644 src/MailKit/Net/Imap/ImapFolderFlags.cs create mode 100644 src/MailKit/Net/Imap/ImapFolderSearch.cs create mode 100644 src/MailKit/Net/Imap/ImapImplementation.cs create mode 100644 src/MailKit/Net/Imap/ImapProtocolException.cs create mode 100644 src/MailKit/Net/Imap/ImapResponseCode.cs create mode 100644 src/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs create mode 100644 src/MailKit/Net/Imap/ImapStream.cs create mode 100644 src/MailKit/Net/Imap/ImapToken.cs create mode 100644 src/MailKit/Net/Imap/ImapUtils.cs create mode 100644 src/MailKit/Net/NetworkStream.cs create mode 100644 src/MailKit/Net/Pop3/AsyncPop3Client.cs create mode 100644 src/MailKit/Net/Pop3/IPop3Client.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Capabilities.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Client.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Command.cs create mode 100644 src/MailKit/Net/Pop3/Pop3CommandException.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Engine.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Language.cs create mode 100644 src/MailKit/Net/Pop3/Pop3ProtocolException.cs create mode 100644 src/MailKit/Net/Pop3/Pop3Stream.cs create mode 100644 src/MailKit/Net/Proxy/HttpProxyClient.cs create mode 100644 src/MailKit/Net/Proxy/IProxyClient.cs create mode 100644 src/MailKit/Net/Proxy/ProxyClient.cs create mode 100644 src/MailKit/Net/Proxy/ProxyProtocolException.cs create mode 100644 src/MailKit/Net/Proxy/Socks4Client.cs create mode 100644 src/MailKit/Net/Proxy/Socks4aClient.cs create mode 100644 src/MailKit/Net/Proxy/Socks5Client.cs create mode 100644 src/MailKit/Net/Proxy/SocksClient.cs create mode 100644 src/MailKit/Net/SelectMode.cs create mode 100644 src/MailKit/Net/Smtp/AsyncSmtpClient.cs create mode 100644 src/MailKit/Net/Smtp/ISmtpClient.cs create mode 100644 src/MailKit/Net/Smtp/SmtpCapabilities.cs create mode 100644 src/MailKit/Net/Smtp/SmtpClient.cs create mode 100644 src/MailKit/Net/Smtp/SmtpCommandException.cs create mode 100644 src/MailKit/Net/Smtp/SmtpDataFilter.cs create mode 100644 src/MailKit/Net/Smtp/SmtpProtocolException.cs create mode 100644 src/MailKit/Net/Smtp/SmtpResponse.cs create mode 100644 src/MailKit/Net/Smtp/SmtpStatusCode.cs create mode 100644 src/MailKit/Net/Smtp/SmtpStream.cs create mode 100644 src/MailKit/Net/SocketUtils.cs create mode 100644 src/MailKit/Net/SslStream.cs create mode 100644 src/MailKit/NullProtocolLogger.cs create mode 100644 src/MailKit/ProgressStream.cs create mode 100644 src/MailKit/Properties/AssemblyInfo.cs create mode 100644 src/MailKit/ProtocolException.cs create mode 100644 src/MailKit/ProtocolLogger.cs create mode 100644 src/MailKit/Search/AnnotationSearchQuery.cs create mode 100644 src/MailKit/Search/BinarySearchQuery.cs create mode 100644 src/MailKit/Search/DateSearchQuery.cs create mode 100644 src/MailKit/Search/FilterSearchQuery.cs create mode 100644 src/MailKit/Search/HeaderSearchQuery.cs create mode 100644 src/MailKit/Search/ISearchQueryOptimizer.cs create mode 100644 src/MailKit/Search/NumericSearchQuery.cs create mode 100644 src/MailKit/Search/OrderBy.cs create mode 100644 src/MailKit/Search/OrderByAnnotation.cs create mode 100644 src/MailKit/Search/OrderByType.cs create mode 100644 src/MailKit/Search/SearchOptions.cs create mode 100644 src/MailKit/Search/SearchQuery.cs create mode 100644 src/MailKit/Search/SearchResults.cs create mode 100644 src/MailKit/Search/SearchTerm.cs create mode 100644 src/MailKit/Search/SortOrder.cs create mode 100644 src/MailKit/Search/TextSearchQuery.cs create mode 100644 src/MailKit/Search/UidSearchQuery.cs create mode 100644 src/MailKit/Search/UnarySearchQuery.cs create mode 100644 src/MailKit/Security/AuthenticationException.cs create mode 100644 src/MailKit/Security/KeyedHashAlgorithm.cs create mode 100644 src/MailKit/Security/Ntlm/BitConverterLE.cs create mode 100644 src/MailKit/Security/Ntlm/ChallengeResponse2.cs create mode 100644 src/MailKit/Security/Ntlm/DES.cs create mode 100644 src/MailKit/Security/Ntlm/HMACMD5.cs create mode 100644 src/MailKit/Security/Ntlm/MD4.cs create mode 100644 src/MailKit/Security/Ntlm/MessageBase.cs create mode 100644 src/MailKit/Security/Ntlm/NtlmAuthLevel.cs create mode 100644 src/MailKit/Security/Ntlm/NtlmFlags.cs create mode 100644 src/MailKit/Security/Ntlm/TargetInfo.cs create mode 100644 src/MailKit/Security/Ntlm/Type1Message.cs create mode 100644 src/MailKit/Security/Ntlm/Type2Message.cs create mode 100644 src/MailKit/Security/Ntlm/Type3Message.cs create mode 100644 src/MailKit/Security/RandomNumberGenerator.cs create mode 100644 src/MailKit/Security/SaslException.cs create mode 100644 src/MailKit/Security/SaslMechanism.cs create mode 100644 src/MailKit/Security/SaslMechanismCramMd5.cs create mode 100644 src/MailKit/Security/SaslMechanismDigestMd5.cs create mode 100644 src/MailKit/Security/SaslMechanismLogin.cs create mode 100644 src/MailKit/Security/SaslMechanismNtlm.cs create mode 100644 src/MailKit/Security/SaslMechanismOAuth2.cs create mode 100644 src/MailKit/Security/SaslMechanismPlain.cs create mode 100644 src/MailKit/Security/SaslMechanismScramBase.cs create mode 100644 src/MailKit/Security/SaslMechanismScramSha1.cs create mode 100644 src/MailKit/Security/SaslMechanismScramSha256.cs create mode 100644 src/MailKit/Security/SecureSocketOptions.cs create mode 100644 src/MailKit/Security/SslHandshakeException.cs create mode 100644 src/MailKit/ServiceNotAuthenticatedException.cs create mode 100644 src/MailKit/ServiceNotConnectedException.cs create mode 100644 src/MailKit/SpecialFolder.cs create mode 100644 src/MailKit/StatusItems.cs create mode 100644 src/MailKit/ThreadingAlgorithm.cs create mode 100644 src/MailKit/UniqueId.cs create mode 100644 src/MailKit/UniqueIdMap.cs create mode 100644 src/MailKit/UniqueIdRange.cs create mode 100644 src/MailKit/UniqueIdSet.cs create mode 100644 src/MailKit/UriExtensions.cs create mode 100644 src/MailKit/mailkit.snk create mode 100644 src/MailKitMailModule.cs create mode 100644 src/MimeKit/AsyncMimeParser.cs create mode 100644 src/MimeKit/AttachmentCollection.cs create mode 100644 src/MimeKit/BodyBuilder.cs create mode 100644 src/MimeKit/CancellationToken.cs create mode 100644 src/MimeKit/ContentDisposition.cs create mode 100644 src/MimeKit/ContentEncoding.cs create mode 100644 src/MimeKit/ContentType.cs create mode 100644 src/MimeKit/Cryptography/ApplicationPgpEncrypted.cs create mode 100644 src/MimeKit/Cryptography/ApplicationPgpSignature.cs create mode 100644 src/MimeKit/Cryptography/ApplicationPkcs7Mime.cs create mode 100644 src/MimeKit/Cryptography/ApplicationPkcs7Signature.cs create mode 100644 src/MimeKit/Cryptography/ArcSigner.cs create mode 100644 src/MimeKit/Cryptography/ArcVerifier.cs create mode 100644 src/MimeKit/Cryptography/AsymmetricAlgorithmExtensions.cs create mode 100644 src/MimeKit/Cryptography/AuthenticationResults.cs create mode 100644 src/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs create mode 100644 src/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/CertificateNotFoundException.cs create mode 100644 src/MimeKit/Cryptography/CmsRecipient.cs create mode 100644 src/MimeKit/Cryptography/CmsRecipientCollection.cs create mode 100644 src/MimeKit/Cryptography/CmsSigner.cs create mode 100644 src/MimeKit/Cryptography/CryptographyContext.cs create mode 100644 src/MimeKit/Cryptography/DbExtensions.cs create mode 100644 src/MimeKit/Cryptography/DefaultSecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/DigestAlgorithm.cs create mode 100644 src/MimeKit/Cryptography/DigitalSignatureCollection.cs create mode 100644 src/MimeKit/Cryptography/DigitalSignatureVerifyException.cs create mode 100644 src/MimeKit/Cryptography/DkimBodyFilter.cs create mode 100644 src/MimeKit/Cryptography/DkimCanonicalizationAlgorithm.cs create mode 100644 src/MimeKit/Cryptography/DkimHashStream.cs create mode 100644 src/MimeKit/Cryptography/DkimPublicKeyLocatorBase.cs create mode 100644 src/MimeKit/Cryptography/DkimRelaxedBodyFilter.cs create mode 100644 src/MimeKit/Cryptography/DkimSignatureAlgorithm.cs create mode 100644 src/MimeKit/Cryptography/DkimSignatureStream.cs create mode 100644 src/MimeKit/Cryptography/DkimSigner.cs create mode 100644 src/MimeKit/Cryptography/DkimSignerBase.cs create mode 100644 src/MimeKit/Cryptography/DkimSimpleBodyFilter.cs create mode 100644 src/MimeKit/Cryptography/DkimVerifier.cs create mode 100644 src/MimeKit/Cryptography/DkimVerifierBase.cs create mode 100644 src/MimeKit/Cryptography/Ed25519DigestSigner.cs create mode 100644 src/MimeKit/Cryptography/EncryptionAlgorithm.cs create mode 100644 src/MimeKit/Cryptography/GnuPGContext.cs create mode 100644 src/MimeKit/Cryptography/IDigitalCertificate.cs create mode 100644 src/MimeKit/Cryptography/IDigitalSignature.cs create mode 100644 src/MimeKit/Cryptography/IDkimPublicKeyLocator.cs create mode 100644 src/MimeKit/Cryptography/IX509CertificateDatabase.cs create mode 100644 src/MimeKit/Cryptography/LdapUri.cs create mode 100644 src/MimeKit/Cryptography/MD5.cs create mode 100644 src/MimeKit/Cryptography/MacSecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/MultipartEncrypted.cs create mode 100644 src/MimeKit/Cryptography/MultipartSigned.cs create mode 100644 src/MimeKit/Cryptography/NpgsqlCertificateDatabase.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpBlockFilter.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpContext.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpContextBase.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpDataType.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpDetectionFilter.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpDigitalCertificate.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpDigitalSignature.cs create mode 100644 src/MimeKit/Cryptography/OpenPgpKeyCertification.cs create mode 100644 src/MimeKit/Cryptography/PrivateKeyNotFoundException.cs create mode 100644 src/MimeKit/Cryptography/PublicKeyAlgorithm.cs create mode 100644 src/MimeKit/Cryptography/PublicKeyNotFoundException.cs create mode 100644 src/MimeKit/Cryptography/RsaEncryptionPadding.cs create mode 100644 src/MimeKit/Cryptography/RsaEncryptionPaddingScheme.cs create mode 100644 src/MimeKit/Cryptography/RsaSignaturePadding.cs create mode 100644 src/MimeKit/Cryptography/RsaSignaturePaddingScheme.cs create mode 100644 src/MimeKit/Cryptography/SecureMailboxAddress.cs create mode 100644 src/MimeKit/Cryptography/SecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs create mode 100644 src/MimeKit/Cryptography/SecureMimeDigitalSignature.cs create mode 100644 src/MimeKit/Cryptography/SecureMimeType.cs create mode 100644 src/MimeKit/Cryptography/SqlCertificateDatabase.cs create mode 100644 src/MimeKit/Cryptography/SqliteCertificateDatabase.cs create mode 100644 src/MimeKit/Cryptography/SubjectIdentifierType.cs create mode 100644 src/MimeKit/Cryptography/TemporarySecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/WindowsSecureMimeContext.cs create mode 100644 src/MimeKit/Cryptography/WindowsSecureMimeDigitalCertificate.cs create mode 100644 src/MimeKit/Cryptography/WindowsSecureMimeDigitalSignature.cs create mode 100644 src/MimeKit/Cryptography/X509Certificate2Extensions.cs create mode 100644 src/MimeKit/Cryptography/X509CertificateChain.cs create mode 100644 src/MimeKit/Cryptography/X509CertificateDatabase.cs create mode 100644 src/MimeKit/Cryptography/X509CertificateRecord.cs create mode 100644 src/MimeKit/Cryptography/X509CertificateStore.cs create mode 100644 src/MimeKit/Cryptography/X509CrlRecord.cs create mode 100644 src/MimeKit/Cryptography/X509KeyUsageFlags.cs create mode 100644 src/MimeKit/DomainList.cs create mode 100644 src/MimeKit/EncodingConstraint.cs create mode 100644 src/MimeKit/Encodings/Base64Decoder.cs create mode 100644 src/MimeKit/Encodings/Base64Encoder.cs create mode 100644 src/MimeKit/Encodings/HexDecoder.cs create mode 100644 src/MimeKit/Encodings/HexEncoder.cs create mode 100644 src/MimeKit/Encodings/IMimeDecoder.cs create mode 100644 src/MimeKit/Encodings/IMimeEncoder.cs create mode 100644 src/MimeKit/Encodings/PassThroughDecoder.cs create mode 100644 src/MimeKit/Encodings/PassThroughEncoder.cs create mode 100644 src/MimeKit/Encodings/QEncoder.cs create mode 100644 src/MimeKit/Encodings/QuotedPrintableDecoder.cs create mode 100644 src/MimeKit/Encodings/QuotedPrintableEncoder.cs create mode 100644 src/MimeKit/Encodings/UUDecoder.cs create mode 100644 src/MimeKit/Encodings/UUEncoder.cs create mode 100644 src/MimeKit/Encodings/YDecoder.cs create mode 100644 src/MimeKit/Encodings/YEncoder.cs create mode 100644 src/MimeKit/FormatOptions.cs create mode 100644 src/MimeKit/GroupAddress.cs create mode 100644 src/MimeKit/Header.cs create mode 100644 src/MimeKit/HeaderId.cs create mode 100644 src/MimeKit/HeaderList.cs create mode 100644 src/MimeKit/HeaderListChangedEventArgs.cs create mode 100644 src/MimeKit/HeaderListCollection.cs create mode 100644 src/MimeKit/IMimeContent.cs create mode 100644 src/MimeKit/IO/BoundStream.cs create mode 100644 src/MimeKit/IO/ChainedStream.cs create mode 100644 src/MimeKit/IO/FilteredStream.cs create mode 100644 src/MimeKit/IO/Filters/ArmoredFromFilter.cs create mode 100644 src/MimeKit/IO/Filters/BestEncodingFilter.cs create mode 100644 src/MimeKit/IO/Filters/CharsetFilter.cs create mode 100644 src/MimeKit/IO/Filters/DecoderFilter.cs create mode 100644 src/MimeKit/IO/Filters/Dos2UnixFilter.cs create mode 100644 src/MimeKit/IO/Filters/EncoderFilter.cs create mode 100644 src/MimeKit/IO/Filters/IMimeFilter.cs create mode 100644 src/MimeKit/IO/Filters/MimeFilterBase.cs create mode 100644 src/MimeKit/IO/Filters/PassThroughFilter.cs create mode 100644 src/MimeKit/IO/Filters/TrailingWhitespaceFilter.cs create mode 100644 src/MimeKit/IO/Filters/Unix2DosFilter.cs create mode 100644 src/MimeKit/IO/ICancellableStream.cs create mode 100644 src/MimeKit/IO/MeasuringStream.cs create mode 100644 src/MimeKit/IO/MemoryBlockStream.cs create mode 100644 src/MimeKit/InternetAddress.cs create mode 100644 src/MimeKit/InternetAddressList.cs create mode 100644 src/MimeKit/MacInterop/CFArray.cs create mode 100644 src/MimeKit/MacInterop/CFData.cs create mode 100644 src/MimeKit/MacInterop/CFDictionary.cs create mode 100644 src/MimeKit/MacInterop/CFObject.cs create mode 100644 src/MimeKit/MacInterop/CFRange.cs create mode 100644 src/MimeKit/MacInterop/CFString.cs create mode 100644 src/MimeKit/MacInterop/CssmDbAttributeFormat.cs create mode 100644 src/MimeKit/MacInterop/CssmKeyUse.cs create mode 100644 src/MimeKit/MacInterop/CssmTPAppleCertStatus.cs create mode 100644 src/MimeKit/MacInterop/Dlfcn.cs create mode 100644 src/MimeKit/MacInterop/OSStatus.cs create mode 100644 src/MimeKit/MacInterop/SecCertificate.cs create mode 100644 src/MimeKit/MacInterop/SecExternalFormat.cs create mode 100644 src/MimeKit/MacInterop/SecItemAttr.cs create mode 100644 src/MimeKit/MacInterop/SecItemClass.cs create mode 100644 src/MimeKit/MacInterop/SecItemExportFlags.cs create mode 100644 src/MimeKit/MacInterop/SecKeyAttribute.cs create mode 100644 src/MimeKit/MacInterop/SecKeychain.cs create mode 100644 src/MimeKit/MacInterop/SecKeychainAttribute.cs create mode 100644 src/MimeKit/MacInterop/SecKeychainAttributeList.cs create mode 100644 src/MimeKit/MailboxAddress.cs create mode 100644 src/MimeKit/MessageDeliveryStatus.cs create mode 100644 src/MimeKit/MessageDispositionNotification.cs create mode 100644 src/MimeKit/MessageIdList.cs create mode 100644 src/MimeKit/MessageImportance.cs create mode 100644 src/MimeKit/MessagePart.cs create mode 100644 src/MimeKit/MessagePartial.cs create mode 100644 src/MimeKit/MessagePriority.cs create mode 100644 src/MimeKit/MimeContent.cs create mode 100644 src/MimeKit/MimeEntity.cs create mode 100644 src/MimeKit/MimeEntityConstructorArgs.cs create mode 100644 src/MimeKit/MimeFormat.cs create mode 100644 src/MimeKit/MimeIterator.cs create mode 100644 src/MimeKit/MimeMessage.cs create mode 100644 src/MimeKit/MimeParser.cs create mode 100644 src/MimeKit/MimePart.cs create mode 100644 src/MimeKit/MimeTypes.cs create mode 100644 src/MimeKit/MimeVisitor.cs create mode 100644 src/MimeKit/Multipart.cs create mode 100644 src/MimeKit/MultipartAlternative.cs create mode 100644 src/MimeKit/MultipartRelated.cs create mode 100644 src/MimeKit/MultipartReport.cs create mode 100644 src/MimeKit/Parameter.cs create mode 100644 src/MimeKit/ParameterEncodingMethod.cs create mode 100644 src/MimeKit/ParameterList.cs create mode 100644 src/MimeKit/ParseException.cs create mode 100644 src/MimeKit/ParserOptions.cs create mode 100644 src/MimeKit/Properties/AssemblyInfo.cs create mode 100644 src/MimeKit/RfcComplianceMode.cs create mode 100644 src/MimeKit/StreamExtensions.cs create mode 100644 src/MimeKit/Text/CharBuffer.cs create mode 100644 src/MimeKit/Text/FlowedToHtml.cs create mode 100644 src/MimeKit/Text/FlowedToText.cs create mode 100644 src/MimeKit/Text/HeaderFooterFormat.cs create mode 100644 src/MimeKit/Text/HtmlAttribute.cs create mode 100644 src/MimeKit/Text/HtmlAttributeCollection.cs create mode 100644 src/MimeKit/Text/HtmlAttributeId.cs create mode 100644 src/MimeKit/Text/HtmlEntityDecoder.cs create mode 100644 src/MimeKit/Text/HtmlEntityDecoder.g.cs create mode 100644 src/MimeKit/Text/HtmlNamespace.cs create mode 100644 src/MimeKit/Text/HtmlTagCallback.cs create mode 100644 src/MimeKit/Text/HtmlTagContext.cs create mode 100644 src/MimeKit/Text/HtmlTagId.cs create mode 100644 src/MimeKit/Text/HtmlTextPreviewer.cs create mode 100644 src/MimeKit/Text/HtmlToHtml.cs create mode 100644 src/MimeKit/Text/HtmlToken.cs create mode 100644 src/MimeKit/Text/HtmlTokenKind.cs create mode 100644 src/MimeKit/Text/HtmlTokenizer.cs create mode 100644 src/MimeKit/Text/HtmlTokenizerState.cs create mode 100644 src/MimeKit/Text/HtmlUtils.cs create mode 100644 src/MimeKit/Text/HtmlWriter.cs create mode 100644 src/MimeKit/Text/HtmlWriterState.cs create mode 100644 src/MimeKit/Text/ICharArray.cs create mode 100644 src/MimeKit/Text/PlainTextPreviewer.cs create mode 100644 src/MimeKit/Text/TextConverter.cs create mode 100644 src/MimeKit/Text/TextFormat.cs create mode 100644 src/MimeKit/Text/TextPreviewer.cs create mode 100644 src/MimeKit/Text/TextToFlowed.cs create mode 100644 src/MimeKit/Text/TextToHtml.cs create mode 100644 src/MimeKit/Text/TextToText.cs create mode 100644 src/MimeKit/Text/Trie.cs create mode 100644 src/MimeKit/Text/UrlScanner.cs create mode 100644 src/MimeKit/TextPart.cs create mode 100644 src/MimeKit/TextRfc822Headers.cs create mode 100644 src/MimeKit/Tnef/RtfCompressedToRtf.cs create mode 100644 src/MimeKit/Tnef/RtfCompressionMode.cs create mode 100644 src/MimeKit/Tnef/TnefAttachFlags.cs create mode 100644 src/MimeKit/Tnef/TnefAttachMethod.cs create mode 100644 src/MimeKit/Tnef/TnefAttributeLevel.cs create mode 100644 src/MimeKit/Tnef/TnefAttributeTag.cs create mode 100644 src/MimeKit/Tnef/TnefComplianceMode.cs create mode 100644 src/MimeKit/Tnef/TnefComplianceStatus.cs create mode 100644 src/MimeKit/Tnef/TnefException.cs create mode 100644 src/MimeKit/Tnef/TnefNameId.cs create mode 100644 src/MimeKit/Tnef/TnefNameIdKind.cs create mode 100644 src/MimeKit/Tnef/TnefPart.cs create mode 100644 src/MimeKit/Tnef/TnefPropertyId.cs create mode 100644 src/MimeKit/Tnef/TnefPropertyReader.cs create mode 100644 src/MimeKit/Tnef/TnefPropertyTag.cs create mode 100644 src/MimeKit/Tnef/TnefPropertyType.cs create mode 100644 src/MimeKit/Tnef/TnefReader.cs create mode 100644 src/MimeKit/Tnef/TnefReaderStream.cs create mode 100644 src/MimeKit/Utils/BufferPool.cs create mode 100644 src/MimeKit/Utils/ByteExtensions.cs create mode 100644 src/MimeKit/Utils/CharsetUtils.cs create mode 100644 src/MimeKit/Utils/Crc32.cs create mode 100644 src/MimeKit/Utils/DateUtils.cs create mode 100644 src/MimeKit/Utils/MimeUtils.cs create mode 100644 src/MimeKit/Utils/OptimizedOrdinalComparer.cs create mode 100644 src/MimeKit/Utils/PackedByteArray.cs create mode 100644 src/MimeKit/Utils/ParseUtils.cs create mode 100644 src/MimeKit/Utils/Rfc2047.cs create mode 100644 src/MimeKit/Utils/StringBuilderExtensions.cs create mode 100644 src/MimeKit/XMessagePriority.cs create mode 100644 src/MimeKit/mimekit.snk diff --git a/src/MailKit/AccessControl.cs b/src/MailKit/AccessControl.cs new file mode 100644 index 0000000..cd7e5bb --- /dev/null +++ b/src/MailKit/AccessControl.cs @@ -0,0 +1,137 @@ +// +// AccessControl.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An Access Control. + /// + /// + /// An Access Control is a set of permissions available for a particular identity, + /// controlling whether or not that identity has the ability to perform various tasks. + /// + /// + /// + /// + public class AccessControl + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new with the given name and + /// access rights. + /// + /// The identifier name. + /// The access rights. + /// + /// is null. + /// -or- + /// is null. + /// + public AccessControl (string name, IEnumerable rights) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Rights = new AccessRights (rights); + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new with the given name and + /// access rights. + /// + /// The identifier name. + /// The access rights. + /// + /// is null. + /// -or- + /// is null. + /// + public AccessControl (string name, string rights) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Rights = new AccessRights (rights); + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new with the given name and no + /// access rights. + /// + /// The identifier name. + /// + /// is null. + /// + public AccessControl (string name) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Rights = new AccessRights (); + Name = name; + } + + /// + /// The identifier name for the access control. + /// + /// + /// The identifier name for the access control. + /// + /// + /// + /// + /// The identifier name. + public string Name { + get; private set; + } + + /// + /// Get the access rights. + /// + /// + /// Gets the access rights. + /// + /// + /// + /// + /// The access rights. + public AccessRights Rights { + get; private set; + } + } +} diff --git a/src/MailKit/AccessControlList.cs b/src/MailKit/AccessControlList.cs new file mode 100644 index 0000000..06ae9c8 --- /dev/null +++ b/src/MailKit/AccessControlList.cs @@ -0,0 +1,66 @@ +// +// AccessControlList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit { + /// + /// An Access Control List (ACL) + /// + /// + /// An Access Control List (ACL) is a list of access controls defining the permissions + /// various identities have available. + /// + /// + /// + /// + public class AccessControlList : List + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The list of access controls. + /// + /// is null. + /// + public AccessControlList (IEnumerable controls) : base (controls) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public AccessControlList () + { + } + } +} diff --git a/src/MailKit/AccessRight.cs b/src/MailKit/AccessRight.cs new file mode 100644 index 0000000..ccbb5db --- /dev/null +++ b/src/MailKit/AccessRight.cs @@ -0,0 +1,232 @@ +// +// AccessRight.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An individual Access Right to be used with ACLs. + /// + /// + /// An individual Access Right meant to be used with + /// . + /// For more information on what rights are available, + /// see https://tools.ietf.org/html/rfc4314#section-2.1 + /// + /// + public struct AccessRight : IEquatable + { + /// + /// The access right for folder lookups. + /// + /// + /// Allows the to be visible when listing folders. + /// + public static readonly AccessRight LookupFolder = new AccessRight ('l'); + + /// + /// The access right for opening a folder and getting the status. + /// + /// + /// Provides access for opening and getting the status of the folder. + /// + public static readonly AccessRight OpenFolder = new AccessRight ('r'); + + /// + /// The access right for adding or removing the Seen flag on messages in the folder. + /// + /// + /// Provides access to add or remove the flag on messages within the + /// . + /// + public static readonly AccessRight SetMessageSeen = new AccessRight ('s'); + + /// + /// The access right for adding or removing flags (other than Seen and Deleted) + /// on messages in a folder. + /// + /// + /// Provides access to add or remove the on messages + /// (other than and + /// ) within the folder. + /// + public static readonly AccessRight SetMessageFlags = new AccessRight ('w'); + + /// + /// The access right allowing messages to be appended or copied into the folder. + /// + /// + /// Provides access to append or copy messages into the folder. + /// + public static readonly AccessRight AppendMessages = new AccessRight ('i'); + + /// + /// The access right allowing subfolders to be created. + /// + /// + /// Provides access to create subfolders. + /// + public static readonly AccessRight CreateFolder = new AccessRight ('k'); + + /// + /// The access right for deleting a folder and/or its subfolders. + /// + /// + /// Provides access to delete the folder and/or any subfolders. + /// + public static readonly AccessRight DeleteFolder = new AccessRight ('x'); + + /// + /// The access right for adding or removing the Deleted flag to messages within a folder. + /// + /// + /// Provides access to add or remove the flag from + /// messages within the folder. It also provides access for setting the + /// flag when appending a message to a folder. + /// + public static readonly AccessRight SetMessageDeleted = new AccessRight ('t'); + + /// + /// The access right for expunging deleted messages in a folder. + /// + /// + /// Provides access to expunge deleted messages in a folder. + /// + public static readonly AccessRight ExpungeFolder = new AccessRight ('e'); + + /// + /// The access right for administering the ACLs of a folder. + /// + /// + /// Provides administrative access to change the ACLs for the folder. + /// + public static readonly AccessRight Administer = new AccessRight ('a'); + + /// + /// The character representing the particular access right. + /// + /// + /// Represents the character value of the access right. + /// + public readonly char Right; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new struct. + /// + /// The access right. + public AccessRight (char right) + { + Right = right; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (AccessRight other) + { + return other.Right == Right; + } + + #endregion + + /// + /// Determines whether two access rights are equal. + /// + /// + /// Determines whether two access rights are equal. + /// + /// true if and are equal; otherwise, false. + /// The first access right to compare. + /// The second access right to compare. + public static bool operator == (AccessRight right1, AccessRight right2) + { + return right1.Right == right2.Right; + } + + /// + /// Determines whether two access rights are not equal. + /// + /// + /// Determines whether two access rights are not equal. + /// + /// true if and are not equal; otherwise, false. + /// The first access right to compare. + /// The second access right to compare. + public static bool operator != (AccessRight right1, AccessRight right2) + { + return right1.Right != right2.Right; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current ; + /// otherwise, false. + public override bool Equals (object obj) + { + return obj is AccessRight && ((AccessRight) obj).Right == Right; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Right.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Right.ToString (); + } + } +} diff --git a/src/MailKit/AccessRights.cs b/src/MailKit/AccessRights.cs new file mode 100644 index 0000000..8a07a71 --- /dev/null +++ b/src/MailKit/AccessRights.cs @@ -0,0 +1,317 @@ +// +// AccessRights.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MailKit { + /// + /// A set of access rights. + /// + /// + /// The set of access rights for a particular identity. + /// + public class AccessRights : ICollection + { + readonly List list = new List (); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new set of access rights. + /// + /// The access rights. + /// + /// is null. + /// + public AccessRights (IEnumerable rights) + { + AddRange (rights); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new set of access rights. + /// + /// The access rights. + /// + /// is null. + /// + public AccessRights (string rights) + { + AddRange (rights); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates an empty set of access rights. + /// + public AccessRights () + { + } + + /// + /// Get the number of access rights in the collection. + /// + /// + /// Gets the number of access rights in the collection. + /// + /// The count. + public int Count { + get { return list.Count; } + } + + /// + /// Get whether or not this set of access rights is read only. + /// + /// + /// Gets whether or not this set of access rights is read only. + /// + /// true if this collection is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add the specified access right. + /// + /// + /// Adds the specified access right if it is not already included. + /// + /// The access right. + void ICollection.Add (AccessRight right) + { + Add (right); + } + + /// + /// Add the specified access right. + /// + /// + /// Adds the specified access right if it is not already included. + /// + /// true if the right was added; otherwise, false. + /// The access right. + public bool Add (AccessRight right) + { + if (list.Contains (right)) + return false; + + list.Add (right); + + return true; + } + + /// + /// Add the specified right. + /// + /// + /// Adds the right specified by the given character. + /// + /// true if the right was added; otherwise, false. + /// The right. + public bool Add (char right) + { + return Add (new AccessRight (right)); + } + + /// + /// Add the rights specified by the characters in the given string. + /// + /// + /// Adds the rights specified by the characters in the given string. + /// + /// The rights. + /// + /// is null. + /// + public void AddRange (string rights) + { + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + for (int i = 0; i < rights.Length; i++) + Add (new AccessRight (rights[i])); + } + + /// + /// Add the range of specified rights. + /// + /// + /// Adds the range of specified rights. + /// + /// The rights. + /// + /// is null. + /// + public void AddRange (IEnumerable rights) + { + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + foreach (var right in rights) + Add (right); + } + + /// + /// Clears the access rights. + /// + /// + /// Removes all of the access rights. + /// + public void Clear () + { + list.Clear (); + } + + /// + /// Checks if the set of access rights contains the specified right. + /// + /// + /// Determines whether or not the set of access rights already contains the specified right + /// + /// true if the specified right exists; otherwise false. + /// The access right. + public bool Contains (AccessRight right) + { + return list.Contains (right); + } + + /// + /// Copies all of the access rights to the specified array. + /// + /// + /// Copies all of the access rights into the array, + /// starting at the specified array index. + /// + /// The array. + /// The array index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (AccessRight[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex + Count > array.Length) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + list.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified access right. + /// + /// + /// Removes the specified access right. + /// + /// true if the access right was removed; otherwise false. + /// The access right. + public bool Remove (AccessRight right) + { + return list.Remove (right); + } + + /// + /// Get the access right at the specified index. + /// + /// + /// Gets the access right at the specified index. + /// + /// The access right at the specified index. + /// The index. + /// + /// is out of range. + /// + public AccessRight this [int index] { + get { + if (index < 0 || index >= list.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return list[index]; + } + } + + #region IEnumerable implementation + + /// + /// Get the access rights enumerator. + /// + /// + /// Gets the access rights enumerator. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return list.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get the access rights enumerator. + /// + /// + /// Gets the access rights enumerator. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return list.GetEnumerator (); + } + + #endregion + + /// + /// Return a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + var rights = new char[list.Count]; + + for (int i = 0; i < list.Count; i++) + rights[i] = list[i].Right; + + return new string (rights); + } + } +} diff --git a/src/MailKit/AlertEventArgs.cs b/src/MailKit/AlertEventArgs.cs new file mode 100644 index 0000000..79c8279 --- /dev/null +++ b/src/MailKit/AlertEventArgs.cs @@ -0,0 +1,69 @@ +// +// AlertEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Alert event arguments. + /// + /// + /// Some implementations, such as + /// , will emit Alert + /// events when they receive alert messages from the server. + /// + public class AlertEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The alert message. + /// + /// is null. + /// + public AlertEventArgs (string message) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + } + + /// + /// Gets the alert message. + /// + /// + /// The alert message will be the exact message received from the server. + /// + /// The alert message. + public string Message { + get; private set; + } + } +} diff --git a/src/MailKit/Annotation.cs b/src/MailKit/Annotation.cs new file mode 100644 index 0000000..c62c61d --- /dev/null +++ b/src/MailKit/Annotation.cs @@ -0,0 +1,81 @@ +// +// Annotation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An annotation. + /// + /// + /// An annotation. + /// For more information about annotations, see + /// rfc5257. + /// + public class Annotation + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The annotation entry. + /// + /// is null. + /// + public Annotation (AnnotationEntry entry) + { + if (entry == null) + throw new ArgumentNullException (nameof (entry)); + + Properties = new Dictionary (); + Entry = entry; + } + + /// + /// Get the annotation tag. + /// + /// + /// Gets the annotation tag. + /// + /// The annotation tag. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation properties. + /// + /// + /// Gets the annotation properties. + /// + public Dictionary Properties { + get; private set; + } + } +} diff --git a/src/MailKit/AnnotationAccess.cs b/src/MailKit/AnnotationAccess.cs new file mode 100644 index 0000000..b4e5f17 --- /dev/null +++ b/src/MailKit/AnnotationAccess.cs @@ -0,0 +1,53 @@ +// +// AnnotationAccess.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// An annotation access level. + /// + /// + /// An annotation access level. + /// For more information about annotations, see + /// rfc5257. + /// + public enum AnnotationAccess + { + /// + /// Annotations are not supported. + /// + None, + + /// + /// Annotations are read-only. + /// + ReadOnly, + + /// + /// Annotations are read-write. + /// + ReadWrite + } +} diff --git a/src/MailKit/AnnotationAttribute.cs b/src/MailKit/AnnotationAttribute.cs new file mode 100644 index 0000000..06a4478 --- /dev/null +++ b/src/MailKit/AnnotationAttribute.cs @@ -0,0 +1,251 @@ +// +// AnnotationAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An annotation attribute. + /// + /// + /// An annotation attribute. + /// For more information about annotations, see + /// rfc5257. + /// + public class AnnotationAttribute : IEquatable + { + static readonly char[] Wildcards = { '*', '%' }; + + /// + /// The annotation value. + /// + /// + /// Used to get or set both the private and shared values of an annotation. + /// + public static readonly AnnotationAttribute Value = new AnnotationAttribute ("value", AnnotationScope.Both); + + /// + /// The shared annotation value. + /// + /// + /// Used to get or set the shared value of an annotation. + /// + public static readonly AnnotationAttribute SharedValue = new AnnotationAttribute ("value", AnnotationScope.Shared); + + /// + /// The private annotation value. + /// + /// + /// Used to get or set the private value of an annotation. + /// + public static readonly AnnotationAttribute PrivateValue = new AnnotationAttribute ("value", AnnotationScope.Private); + + /// + /// The size of an annotation value. + /// + /// + /// Used to get the size of the both the private and shared annotation values. + /// + public static readonly AnnotationAttribute Size = new AnnotationAttribute ("size", AnnotationScope.Both); + + /// + /// The size of a shared annotation value. + /// + /// + /// Used to get the size of a shared annotation value. + /// + public static readonly AnnotationAttribute SharedSize = new AnnotationAttribute ("size", AnnotationScope.Shared); + + /// + /// The size of a private annotation value. + /// + /// + /// Used to get the size of a private annotation value. + /// + public static readonly AnnotationAttribute PrivateSize = new AnnotationAttribute ("size", AnnotationScope.Private); + + AnnotationAttribute (string name, AnnotationScope scope) + { + switch (scope) { + case AnnotationScope.Shared: Specifier = string.Format ("{0}.shared", name); break; + case AnnotationScope.Private: Specifier = string.Format ("{0}.priv", name); break; + default: Specifier = name; break; + } + Scope = scope; + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The annotation attribute specifier. + /// + /// is null. + /// + /// + /// contains illegal characters. + /// + public AnnotationAttribute (string specifier) + { + if (specifier == null) + throw new ArgumentNullException (nameof (specifier)); + + if (specifier.Length == 0) + throw new ArgumentException ("Annotation attribute specifiers cannot be empty.", nameof (specifier)); + + // TODO: improve validation + if (specifier.IndexOfAny (Wildcards) != -1) + throw new ArgumentException ("Annotation attribute specifiers cannot contain '*' or '%'.", nameof (specifier)); + + Specifier = specifier; + + if (specifier.EndsWith (".shared", StringComparison.Ordinal)) { + Name = specifier.Substring (0, specifier.Length - ".shared".Length); + Scope = AnnotationScope.Shared; + } else if (specifier.EndsWith (".priv", StringComparison.Ordinal)) { + Name = specifier.Substring (0, specifier.Length - ".priv".Length); + Scope = AnnotationScope.Private; + } else { + Scope = AnnotationScope.Both; + Name = specifier; + } + } + + /// + /// Get the name of the annotation attribute. + /// + /// + /// Gets the name of the annotation attribute. + /// + public string Name { + get; private set; + } + + /// + /// Get the scope of the annotation attribute. + /// + /// + /// Gets the scope of the annotation attribute. + /// + public AnnotationScope Scope { + get; private set; + } + + /// + /// Get the annotation attribute specifier. + /// + /// + /// Gets the annotation attribute specifier. + /// + public string Specifier { + get; private set; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (AnnotationAttribute other) + { + return other?.Specifier == Specifier; + } + + #endregion + + /// + /// Determines whether two annotation attributes are equal. + /// + /// + /// Determines whether two annotation attributes are equal. + /// + /// true if and are equal; otherwise, false. + /// The first annotation attribute to compare. + /// The second annotation attribute to compare. + public static bool operator == (AnnotationAttribute attr1, AnnotationAttribute attr2) + { + return attr1?.Specifier == attr2?.Specifier; + } + + /// + /// Determines whether two annotation attributes are not equal. + /// + /// + /// Determines whether two annotation attributes are not equal. + /// + /// true if and are not equal; otherwise, false. + /// The first annotation attribute to compare. + /// The second annotation attribute to compare. + public static bool operator != (AnnotationAttribute attr1, AnnotationAttribute attr2) + { + return attr1?.Specifier != attr2?.Specifier; + } + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals (object obj) + { + return obj is AnnotationAttribute && ((AnnotationAttribute) obj).Specifier == Specifier; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Specifier.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Specifier; + } + } +} diff --git a/src/MailKit/AnnotationEntry.cs b/src/MailKit/AnnotationEntry.cs new file mode 100644 index 0000000..8f510e1 --- /dev/null +++ b/src/MailKit/AnnotationEntry.cs @@ -0,0 +1,521 @@ +// +// Annotationentry.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An annotation entry. + /// + /// + /// An annotation entry. + /// For more information about annotations, see + /// rfc5257. + /// + public class AnnotationEntry : IEquatable + { + /// + /// An annotation entry for a comment on a message. + /// + /// + /// Used to get or set a comment on a message. + /// + public static readonly AnnotationEntry Comment = new AnnotationEntry ("/comment", AnnotationScope.Both); + + /// + /// An annotation entry for a private comment on a message. + /// + /// + /// Used to get or set a private comment on a message. + /// + public static readonly AnnotationEntry PrivateComment = new AnnotationEntry ("/comment", AnnotationScope.Private); + + /// + /// An annotation entry for a shared comment on a message. + /// + /// + /// Used to get or set a shared comment on a message. + /// + public static readonly AnnotationEntry SharedComment = new AnnotationEntry ("/comment", AnnotationScope.Shared); + + /// + /// An annotation entry for flags on a message. + /// + /// + /// Used to get or set flags on a message. + /// + public static readonly AnnotationEntry Flags = new AnnotationEntry ("/flags", AnnotationScope.Both); + + /// + /// An annotation entry for private flags on a message. + /// + /// + /// Used to get or set private flags on a message. + /// + public static readonly AnnotationEntry PrivateFlags = new AnnotationEntry ("/flags", AnnotationScope.Private); + + /// + /// Aa annotation entry for shared flags on a message. + /// + /// + /// Used to get or set shared flags on a message. + /// + public static readonly AnnotationEntry SharedFlags = new AnnotationEntry ("/flags", AnnotationScope.Shared); + + /// + /// An annotation entry for an alternate subject on a message. + /// + /// + /// Used to get or set an alternate subject on a message. + /// + public static readonly AnnotationEntry AltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Both); + + /// + /// An annotation entry for a private alternate subject on a message. + /// + /// + /// Used to get or set a private alternate subject on a message. + /// + public static readonly AnnotationEntry PrivateAltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Private); + + /// + /// An annotation entry for a shared alternate subject on a message. + /// + /// + /// Used to get or set a shared alternate subject on a message. + /// + public static readonly AnnotationEntry SharedAltSubject = new AnnotationEntry ("/altsubject", AnnotationScope.Shared); + + static void ValidatePath (string path) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + if (path.Length == 0) + throw new ArgumentException ("Annotation entry paths cannot be empty.", nameof (path)); + + if (path[0] != '/' && path[0] != '*' && path[0] != '%') + throw new ArgumentException ("Annotation entry paths must begin with '/'.", nameof (path)); + + if (path.Length > 1 && path[1] >= '0' && path[1] <= '9') + throw new ArgumentException ("Annotation entry paths must not include a part-specifier.", nameof (path)); + + if (path == "*" || path == "%") + return; + + char pc = path[0]; + + for (int i = 1; i < path.Length; i++) { + char c = path[i]; + + if (c > 127) + throw new ArgumentException ($"Invalid character in annotation entry path: '{c}'.", nameof (path)); + + if (c >= '0' && c <= '9' && pc == '/') + throw new ArgumentException ("Invalid annotation entry path.", nameof (path)); + + if ((pc == '/' || pc == '.') && (c == '/' || c == '.')) + throw new ArgumentException ("Invalid annotation entry path.", nameof (path)); + + pc = c; + } + + int endIndex = path.Length - 1; + + if (path[endIndex] == '/') + throw new ArgumentException ("Annotation entry paths must not end with '/'.", nameof (path)); + + if (path[endIndex] == '.') + throw new ArgumentException ("Annotation entry paths must not end with '.'.", nameof (path)); + } + + static void ValidatePartSpecifier (string partSpecifier) + { + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + char pc = '\0'; + + for (int i = 0; i < partSpecifier.Length; i++) { + char c = partSpecifier[i]; + + if (!((c >= '0' && c <= '9') || c == '.') || (c == '.' && (pc == '.' || pc == '\0'))) + throw new ArgumentException ("Invalid part-specifier.", nameof (partSpecifier)); + + pc = c; + } + + if (pc == '.') + throw new ArgumentException ("Invalid part-specifier.", nameof (partSpecifier)); + } + + AnnotationEntry () + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new . + /// + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is null. + /// + /// + /// is invalid. + /// + public AnnotationEntry (string path, AnnotationScope scope = AnnotationScope.Both) + { + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = path + ".priv"; break; + case AnnotationScope.Shared: Entry = path + ".shared"; break; + default: Entry = path; break; + } + PartSpecifier = null; + Path = path; + Scope = scope; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new for an individual body part of a message. + /// + /// The part-specifier of the body part of the message. + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// -or- + /// is invalid. + /// + public AnnotationEntry (string partSpecifier, string path, AnnotationScope scope = AnnotationScope.Both) + { + ValidatePartSpecifier (partSpecifier); + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = string.Format ("/{0}{1}.priv", partSpecifier, path); break; + case AnnotationScope.Shared: Entry = string.Format ("/{0}{1}.shared", partSpecifier, path); break; + default: Entry = string.Format ("/{0}{1}", partSpecifier, path); break; + } + PartSpecifier = partSpecifier; + Path = path; + Scope = scope; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new for an individual body part of a message. + /// + /// The body part of the message. + /// The annotation entry path. + /// The scope of the annotation. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + public AnnotationEntry (BodyPart part, string path, AnnotationScope scope = AnnotationScope.Both) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + ValidatePath (path); + + switch (scope) { + case AnnotationScope.Private: Entry = string.Format ("/{0}{1}.priv", part.PartSpecifier, path); break; + case AnnotationScope.Shared: Entry = string.Format ("/{0}{1}.shared", part.PartSpecifier, path); break; + default: Entry = string.Format ("/{0}{1}", part.PartSpecifier, path); break; + } + PartSpecifier = part.PartSpecifier; + Path = path; + Scope = scope; + } + + /// + /// Get the annotation entry specifier. + /// + /// + /// Gets the annotation entry specifier. + /// + /// The annotation entry specifier. + public string Entry { + get; private set; + } + + /// + /// Get the part-specifier component of the annotation entry. + /// + /// + /// Gets the part-specifier component of the annotation entry. + /// + public string PartSpecifier { + get; private set; + } + + /// + /// Get the path component of the annotation entry. + /// + /// + /// Gets the path component of the annotation entry. + /// + public string Path { + get; private set; + } + + /// + /// Get the scope of the annotation. + /// + /// + /// Gets the scope of the annotation. + /// + public AnnotationScope Scope { + get; private set; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (AnnotationEntry other) + { + return other?.Entry == Entry; + } + + #endregion + + /// + /// Determines whether two annotation entries are equal. + /// + /// + /// Determines whether two annotation entries are equal. + /// + /// true if and are equal; otherwise, false. + /// The first annotation entry to compare. + /// The second annotation entry to compare. + public static bool operator == (AnnotationEntry entry1, AnnotationEntry entry2) + { + return entry1?.Entry == entry2?.Entry; + } + + /// + /// Determines whether two annotation entries are not equal. + /// + /// + /// Determines whether two annotation entries are not equal. + /// + /// true if and are not equal; otherwise, false. + /// The first annotation entry to compare. + /// The second annotation entry to compare. + public static bool operator != (AnnotationEntry entry1, AnnotationEntry entry2) + { + return entry1?.Entry != entry2?.Entry; + } + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals (object obj) + { + return obj is AnnotationEntry && ((AnnotationEntry) obj).Entry == Entry; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Entry.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Entry; + } + + /// + /// Parse an annotation entry. + /// + /// + /// Parses an annotation entry. + /// + /// The annotation entry. + /// The parsed annotation entry. + /// + /// is null. + /// + /// + /// does not conform to the annotation entry syntax. + /// + public static AnnotationEntry Parse (string entry) + { + if (entry == null) + throw new ArgumentNullException (nameof (entry)); + + if (entry.Length == 0) + throw new FormatException ("An annotation entry cannot be empty."); + + if (entry[0] != '/' && entry[0] != '*' && entry[0] != '%') + throw new FormatException ("An annotation entry must begin with a '/' character."); + + var scope = AnnotationScope.Both; + int startIndex = 0, endIndex; + string partSpecifier = null; + var component = 0; + var pc = entry[0]; + string path; + + for (int i = 1; i < entry.Length; i++) { + char c = entry[i]; + + if (c >= '0' && c <= '9' && pc == '/') { + if (component > 0) + throw new FormatException ("Invalid annotation entry."); + + startIndex = i; + endIndex = i + 1; + pc = c; + + while (endIndex < entry.Length) { + c = entry[endIndex]; + + if (c == '/') { + if (pc == '.') + throw new FormatException ("Invalid part-specifier in annotation entry."); + + break; + } + + if (!(c >= '0' && c <= '9') && c != '.') + throw new FormatException ($"Invalid character in part-specifier: '{c}'."); + + if (c == '.' && pc == '.') + throw new FormatException ("Invalid part-specifier in annotation entry."); + + endIndex++; + pc = c; + } + + if (endIndex >= entry.Length) + throw new FormatException ("Incomplete part-specifier in annotation entry."); + + partSpecifier = entry.Substring (startIndex, endIndex - startIndex); + i = startIndex = endIndex; + component++; + } else if (c == '/' || c == '.') { + if (pc == '/' || pc == '.') + throw new FormatException ("Invalid annotation entry path."); + + if (c == '/') + component++; + } else if (c > 127) { + throw new FormatException ($"Invalid character in annotation entry path: '{c}'."); + } + + pc = c; + } + + if (pc == '/' || pc == '.') + throw new FormatException ("Invalid annotation entry path."); + + if (entry.EndsWith (".shared", StringComparison.Ordinal)) { + endIndex = entry.Length - ".shared".Length; + scope = AnnotationScope.Shared; + } else if (entry.EndsWith (".priv", StringComparison.Ordinal)) { + endIndex = entry.Length - ".priv".Length; + scope = AnnotationScope.Private; + } else { + endIndex = entry.Length; + } + + path = entry.Substring (startIndex, endIndex - startIndex); + + return new AnnotationEntry { + PartSpecifier = partSpecifier, + Entry = entry, + Path = path, + Scope = scope + }; + } + + internal static AnnotationEntry Create (string entry) + { + switch (entry) { + case "/comment": return Comment; + case "/comment.priv": return PrivateComment; + case "/comment.shared": return SharedComment; + case "/flags": return Flags; + case "/flags.priv": return PrivateFlags; + case "/flags.shared": return SharedFlags; + case "/altsubject": return AltSubject; + case "/altsubject.priv": return PrivateAltSubject; + case "/altsubject.shared": return SharedAltSubject; + default: return Parse (entry); + } + } + } +} diff --git a/src/MailKit/AnnotationScope.cs b/src/MailKit/AnnotationScope.cs new file mode 100644 index 0000000..c091a70 --- /dev/null +++ b/src/MailKit/AnnotationScope.cs @@ -0,0 +1,61 @@ +// +// AnnotationScope.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// The scope of an annotation. + /// + /// + /// Represents the scope of an annotation. + /// For more information about annotations, see + /// rfc5257. + /// + [Flags] + public enum AnnotationScope + { + /// + /// No scopes. + /// + None, + + /// + /// The private annotation scope. + /// + Private, + + /// + /// The shared annotation scope. + /// + Shared, + + /// + /// Both private and shared scopes. + /// + Both = Private | Shared + } +} diff --git a/src/MailKit/AnnotationsChangedEventArgs.cs b/src/MailKit/AnnotationsChangedEventArgs.cs new file mode 100644 index 0000000..b3d4015 --- /dev/null +++ b/src/MailKit/AnnotationsChangedEventArgs.cs @@ -0,0 +1,93 @@ +// +// AnnotationsChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MailKit { + /// + /// Event args used when an annotation changes. + /// + /// + /// Event args used when an annotation changes. + /// + public class AnnotationsChangedEventArgs : MessageEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + internal AnnotationsChangedEventArgs (int index) : base (index) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The annotations that changed. + /// + /// is null. + /// + public AnnotationsChangedEventArgs (int index, IEnumerable annotations) : base (index) + { + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + Annotations = new ReadOnlyCollection (annotations.ToArray ()); + } + + /// + /// Get the annotations that changed. + /// + /// + /// Gets the annotations that changed. + /// + /// The annotation. + public IList Annotations { + get; internal set; + } + + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// The mod-sequence value. + public ulong? ModSeq { + get; internal set; + } + } +} diff --git a/src/MailKit/AuthenticatedEventArgs.cs b/src/MailKit/AuthenticatedEventArgs.cs new file mode 100644 index 0000000..5f9467a --- /dev/null +++ b/src/MailKit/AuthenticatedEventArgs.cs @@ -0,0 +1,68 @@ +// +// AuthenticatedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Authenticated event arguments. + /// + /// + /// Some servers, such as GMail IMAP, will send some free-form text in + /// the response to a successful login. + /// + public class AuthenticatedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The free-form text. + /// + /// is null. + /// + public AuthenticatedEventArgs (string message) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + } + + /// + /// Get the free-form text sent by the server. + /// + /// + /// Gets the free-form text sent by the server. + /// + /// The free-form text sent by the server. + public string Message { + get; private set; + } + } +} diff --git a/src/MailKit/BodyPart.cs b/src/MailKit/BodyPart.cs new file mode 100644 index 0000000..3d3a548 --- /dev/null +++ b/src/MailKit/BodyPart.cs @@ -0,0 +1,716 @@ +// +// BodyPart.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.Utils; + +namespace MailKit { + /// + /// An abstract body part of a message. + /// + /// + /// Each body part will actually be a , + /// , , or + /// . + /// + /// + /// + /// + public abstract class BodyPart + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + protected BodyPart () + { + } + + /// + /// Gets the Content-Type of the body part. + /// + /// + /// Gets the Content-Type of the body part. + /// + /// The content type. + public ContentType ContentType { + get; set; + } + + /// + /// Gets the part specifier. + /// + /// + /// Gets the part specifier. + /// + /// + /// + /// + /// The part specifier. + public string PartSpecifier { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME body part. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public abstract void Accept (BodyPartVisitor visitor); + + internal static void Encode (StringBuilder builder, uint value) + { + builder.Append (value.ToString ()); + } + + internal static void Encode (StringBuilder builder, string value) + { + if (value != null) + builder.Append (MimeUtils.Quote (value)); + else + builder.Append ("NIL"); + } + + internal static void Encode (StringBuilder builder, Uri location) + { + if (location != null) + builder.Append (MimeUtils.Quote (location.ToString ())); + else + builder.Append ("NIL"); + } + + internal static void Encode (StringBuilder builder, string[] values) + { + if (values == null || values.Length == 0) { + builder.Append ("NIL"); + return; + } + + builder.Append ('('); + + for (int i = 0; i < values.Length; i++) { + if (i > 0) + builder.Append (' '); + + Encode (builder, values[i]); + } + + builder.Append (')'); + } + + internal static void Encode (StringBuilder builder, IList parameters) + { + if (parameters == null || parameters.Count == 0) { + builder.Append ("NIL"); + return; + } + + builder.Append ('('); + + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) + builder.Append (' '); + + Encode (builder, parameters[i].Name); + builder.Append (' '); + Encode (builder, parameters[i].Value); + } + + builder.Append (')'); + } + + internal static void Encode (StringBuilder builder, ContentDisposition disposition) + { + if (disposition == null) { + builder.Append ("NIL"); + return; + } + + builder.Append ('('); + Encode (builder, disposition.Disposition); + builder.Append (' '); + Encode (builder, disposition.Parameters); + builder.Append (')'); + } + + internal static void Encode (StringBuilder builder, ContentType contentType) + { + Encode (builder, contentType.MediaType); + builder.Append (' '); + Encode (builder, contentType.MediaSubtype); + builder.Append (' '); + Encode (builder, contentType.Parameters); + } + + internal static void Encode (StringBuilder builder, BodyPartCollection parts) + { + if (parts == null || parts.Count == 0) { + builder.Append ("NIL"); + return; + } + + for (int i = 0; i < parts.Count; i++) { + if (i > 0) + builder.Append (' '); + + Encode (builder, parts[i]); + } + } + + internal static void Encode (StringBuilder builder, Envelope envelope) + { + if (envelope == null) { + builder.Append ("NIL"); + return; + } + + envelope.Encode (builder); + } + + internal static void Encode (StringBuilder builder, BodyPart body) + { + if (body == null) { + builder.Append ("NIL"); + return; + } + + builder.Append ('('); + body.Encode (builder); + builder.Append (')'); + } + + /// + /// Encodes the into the . + /// + /// + /// Encodes the into the . + /// + /// The string builder. + protected abstract void Encode (StringBuilder builder); + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// The syntax of the string returned, while similar to IMAP's BODYSTRUCTURE syntax, + /// is not completely compatible. + /// + /// A that represents the current . + public override string ToString () + { + var builder = new StringBuilder (); + + builder.Append ('('); + Encode (builder); + builder.Append (')'); + + return builder.ToString (); + } + + static bool TryParse (string text, ref int index, out uint value) + { + while (index < text.Length && text[index] == ' ') + index++; + + int startIndex = index; + + value = 0; + + while (index < text.Length && char.IsDigit (text[index])) + value = (value * 10) + (uint) (text[index++] - '0'); + + return index > startIndex; + } + + static bool TryParse (string text, ref int index, out string nstring) + { + nstring = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '"') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + var token = new StringBuilder (); + bool escaped = false; + + index++; + + while (index < text.Length) { + if (text[index] == '"' && !escaped) + break; + + if (escaped || text[index] != '\\') { + token.Append (text[index]); + escaped = false; + } else { + escaped = true; + } + + index++; + } + + if (index >= text.Length) + return false; + + nstring = token.ToString (); + + index++; + + return true; + } + + static bool TryParse (string text, ref int index, out string[] values) + { + values = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + index++; + + if (index >= text.Length) + return false; + + var list = new List (); + string value; + + do { + if (text[index] == ')') + break; + + if (!TryParse (text, ref index, out value)) + return false; + + list.Add (value); + } while (index < text.Length); + + if (index >= text.Length || text[index] != ')') + return false; + + values = list.ToArray (); + index++; + + return true; + } + + static bool TryParse (string text, ref int index, out Uri uri) + { + string nstring; + + uri = null; + + if (!TryParse (text, ref index, out nstring)) + return false; + + if (!string.IsNullOrEmpty (nstring)) { + if (Uri.IsWellFormedUriString (nstring, UriKind.Absolute)) + uri = new Uri (nstring, UriKind.Absolute); + else if (Uri.IsWellFormedUriString (nstring, UriKind.Relative)) + uri = new Uri (nstring, UriKind.Relative); + } + + return true; + } + + static bool TryParse (string text, ref int index, out IList parameters) + { + string name, value; + + parameters = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + parameters = new List (); + index += 3; + return true; + } + + return false; + } + + index++; + + if (index >= text.Length) + return false; + + parameters = new List (); + + do { + if (text[index] == ')') + break; + + if (!TryParse (text, ref index, out name)) + return false; + + if (!TryParse (text, ref index, out value)) + return false; + + parameters.Add (new Parameter (name, value)); + } while (index < text.Length); + + if (index >= text.Length || text[index] != ')') + return false; + + index++; + + return true; + } + + static bool TryParse (string text, ref int index, out ContentDisposition disposition) + { + IList parameters; + string value; + + disposition = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + index++; + + if (!TryParse (text, ref index, out value)) + return false; + + if (!TryParse (text, ref index, out parameters)) + return false; + + if (index >= text.Length || text[index] != ')') + return false; + + index++; + + disposition = new ContentDisposition (value); + + foreach (var param in parameters) + disposition.Parameters.Add (param); + + return true; + } + + static bool TryParse (string text, ref int index, bool multipart, out ContentType contentType) + { + IList parameters; + string type, subtype; + + contentType = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (!multipart) { + if (!TryParse (text, ref index, out type)) + return false; + } else { + type = "multipart"; + } + + if (!TryParse (text, ref index, out subtype)) + return false; + + if (!TryParse (text, ref index, out parameters)) + return false; + + contentType = new ContentType (type ?? "application", subtype ?? "octet-stream"); + + foreach (var param in parameters) + contentType.Parameters.Add (param); + + return true; + } + + static bool TryParse (string text, ref int index, string prefix, out IList children) + { + BodyPart part; + string path; + int id = 1; + + children = null; + + if (index >= text.Length) + return false; + + children = new List (); + + do { + if (text[index] != '(') + break; + + path = prefix + id; + + if (!TryParse (text, ref index, path, out part)) + return false; + + while (index < text.Length && text[index] == ' ') + index++; + + children.Add (part); + id++; + } while (index < text.Length); + + return index < text.Length; + } + + static bool TryParse (string text, ref int index, string path, out BodyPart part) + { + ContentDisposition disposition; + ContentType contentType; + string[] array; + string nstring; + Uri location; + uint number; + + part = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length || text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + index++; + + if (index >= text.Length) + return false; + + if (text[index] == '(') { + var prefix = path.Length > 0 ? path + "." : string.Empty; + var multipart = new BodyPartMultipart (); + IList children; + + if (!TryParse (text, ref index, prefix, out children)) + return false; + + foreach (var child in children) + multipart.BodyParts.Add (child); + + if (!TryParse (text, ref index, true, out contentType)) + return false; + + multipart.ContentType = contentType; + + if (!TryParse (text, ref index, out disposition)) + return false; + + multipart.ContentDisposition = disposition; + + if (!TryParse (text, ref index, out array)) + return false; + + multipart.ContentLanguage = array; + + if (!TryParse (text, ref index, out location)) + return false; + + multipart.ContentLocation = location; + + part = multipart; + } else { + BodyPartMessage message = null; + BodyPartText txt = null; + BodyPartBasic basic; + + if (!TryParse (text, ref index, false, out contentType)) + return false; + + if (contentType.IsMimeType ("message", "rfc822")) + basic = message = new BodyPartMessage (); + else if (contentType.IsMimeType ("text", "*")) + basic = txt = new BodyPartText (); + else + basic = new BodyPartBasic (); + + basic.ContentType = contentType; + + if (!TryParse (text, ref index, out nstring)) + return false; + + basic.ContentId = nstring; + + if (!TryParse (text, ref index, out nstring)) + return false; + + basic.ContentDescription = nstring; + + if (!TryParse (text, ref index, out nstring)) + return false; + + basic.ContentTransferEncoding = nstring; + + if (!TryParse (text, ref index, out number)) + return false; + + basic.Octets = number; + + if (!TryParse (text, ref index, out nstring)) + return false; + + basic.ContentMd5 = nstring; + + if (!TryParse (text, ref index, out disposition)) + return false; + + basic.ContentDisposition = disposition; + + if (!TryParse (text, ref index, out array)) + return false; + + basic.ContentLanguage = array; + + if (!TryParse (text, ref index, out location)) + return false; + + basic.ContentLocation = location; + + if (message != null) { + Envelope envelope; + BodyPart body; + + if (!Envelope.TryParse (text, ref index, out envelope)) + return false; + + message.Envelope = envelope; + + if (!TryParse (text, ref index, path, out body)) + return false; + + message.Body = body; + + if (!TryParse (text, ref index, out number)) + return false; + + message.Lines = number; + } else if (txt != null) { + if (!TryParse (text, ref index, out number)) + return false; + + txt.Lines = number; + } + + part = basic; + } + + part.PartSpecifier = path; + + if (index >= text.Length || text[index] != ')') + return false; + + index++; + + return true; + } + + /// + /// Tries to parse the given text into a new instance. + /// + /// + /// Parses a body part from the specified text. + /// This syntax, while similar to IMAP's BODYSTRUCTURE syntax, is not completely + /// compatible. + /// + /// true, if the body part was successfully parsed, false otherwise. + /// The text to parse. + /// The parsed body part. + /// + /// is null. + /// + public static bool TryParse (string text, out BodyPart part) + { + if (text == null) + throw new ArgumentNullException (nameof (text)); + + int index = 0; + + return TryParse (text, ref index, string.Empty, out part) && index == text.Length; + } + } +} diff --git a/src/MailKit/BodyPartBasic.cs b/src/MailKit/BodyPartBasic.cs new file mode 100644 index 0000000..17ffdf6 --- /dev/null +++ b/src/MailKit/BodyPartBasic.cs @@ -0,0 +1,245 @@ +// +// BodyPartBasic.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit; + +namespace MailKit { + /// + /// A basic message body part. + /// + /// + /// Represents any message body part that is not a multipart, + /// message/rfc822 part, or a text part. + /// + /// + /// + /// + public class BodyPartBasic : BodyPart + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public BodyPartBasic () + { + } + + /// + /// Gets the Content-Id of the body part, if available. + /// + /// + /// Gets the Content-Id of the body part, if available. + /// + /// The content identifier. + public string ContentId { + get; set; + } + + /// + /// Gets the Content-Description of the body part, if available. + /// + /// + /// Gets the Content-Description of the body part, if available. + /// + /// The content description. + public string ContentDescription { + get; set; + } + + /// + /// Gets the Content-Transfer-Encoding of the body part. + /// + /// + /// Gets the Content-Transfer-Encoding of the body part. + /// Hint: Use the MimeUtils.TryParse + /// method to parse this value into a usable . + /// + /// The content transfer encoding. + public string ContentTransferEncoding { + get; set; + } + + /// + /// Gets the size of the body part, in bytes. + /// + /// + /// Gets the size of the body part, in bytes. + /// + /// The number of octets. + public uint Octets { + get; set; + } + + /// + /// Gets the MD5 hash of the content, if available. + /// + /// + /// Gets the MD5 hash of the content, if available. + /// + /// The content md5. + public string ContentMd5 { + get; set; + } + + /// + /// Gets the Content-Disposition of the body part, if available. + /// + /// + /// Gets the Content-Disposition of the body part, if available. + /// The Content-Disposition value is only retrieved if the + /// flag is used when fetching + /// summary information from an . + /// + /// The content disposition. + public ContentDisposition ContentDisposition { + get; set; + } + + /// + /// Gets the Content-Language of the body part, if available. + /// + /// + /// Gets the Content-Language of the body part, if available. + /// The Content-Language value is only retrieved if the + /// flag is used when fetching + /// summary information from an . + /// + /// The content language. + public string[] ContentLanguage { + get; set; + } + + /// + /// Gets the Content-Location of the body part, if available. + /// + /// + /// Gets the Content-Location of the body part, if available. + /// The Content-Location value is only retrieved if the + /// flag is used when fetching + /// summary information from an . + /// + /// The content location. + public Uri ContentLocation { + get; set; + } + + /// + /// Determines whether or not the body part is an attachment. + /// + /// + /// Determines whether or not the body part is an attachment based on the value of + /// the Content-Disposition. + /// Since the value of the Content-Disposition header is needed, it + /// is necessary to include the flag when + /// fetching summary information from an . + /// + /// true if this part is an attachment; otherwise, false. + public bool IsAttachment { + get { return ContentDisposition != null && ContentDisposition.IsAttachment; } + } + + /// + /// Get the name of the file. + /// + /// + /// First checks for the "filename" parameter on the Content-Disposition header. If + /// that does not exist, then the "name" parameter on the Content-Type header is used. + /// Since the value of the Content-Disposition header is needed, it is + /// necessary to include the flag when + /// fetching summary information from an . + /// + /// The name of the file. + public string FileName { + get { + string filename = null; + + if (ContentDisposition != null) + filename = ContentDisposition.FileName; + + if (filename == null) + filename = ContentType.Name; + + return filename != null ? filename.Trim () : null; + } + } + + /// + /// Dispatches to the specific visit method for this MIME body part. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (BodyPartVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitBodyPartBasic (this); + } + + /// + /// Encodes the into the . + /// + /// + /// Encodes the into the . + /// + /// The string builder. + protected override void Encode (StringBuilder builder) + { + Encode (builder, ContentType); + builder.Append (' '); + Encode (builder, ContentId); + builder.Append (' '); + Encode (builder, ContentDescription); + builder.Append (' '); + Encode (builder, ContentTransferEncoding); + builder.Append (' '); + Encode (builder, Octets); + builder.Append (' '); + Encode (builder, ContentMd5); + builder.Append (' '); + Encode (builder, ContentDisposition); + builder.Append (' '); + Encode (builder, ContentLanguage); + builder.Append (' '); + Encode (builder, ContentLocation); + } + } +} diff --git a/src/MailKit/BodyPartCollection.cs b/src/MailKit/BodyPartCollection.cs new file mode 100644 index 0000000..bbd78bb --- /dev/null +++ b/src/MailKit/BodyPartCollection.cs @@ -0,0 +1,276 @@ +// +// BodyPartCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MailKit { + /// + /// A collection. + /// + /// + /// A collection. + /// + public class BodyPartCollection : ICollection + { + readonly List collection = new List (); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public BodyPartCollection () + { + } + + /// + /// Get the number of body parts in the collection. + /// + /// + /// Gets the number of body parts in the collection. + /// + /// The count. + public int Count { + get { return collection.Count; } + } + + /// + /// Get whether or not this body part collection is read only. + /// + /// + /// Gets whether or not this body part collection is read only. + /// + /// true if this collection is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add the specified body part to the collection. + /// + /// + /// Adds the specified body part to the collection. + /// + /// The body part. + /// + /// is null. + /// + public void Add (BodyPart part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + collection.Add (part); + } + + /// + /// Clears the body part collection. + /// + /// + /// Removes all of the body parts from the collection. + /// + public void Clear () + { + collection.Clear (); + } + + /// + /// Checks if the collection contains the specified body part. + /// + /// + /// Determines whether or not the collection contains the specified body part. + /// + /// true if the specified body part exists; otherwise false. + /// The body part. + /// + /// is null. + /// + public bool Contains (BodyPart part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return collection.Contains (part); + } + + /// + /// Copies all of the body parts in the collection to the specified array. + /// + /// + /// Copies all of the body parts within the collection into the array, + /// starting at the specified array index. + /// + /// The array. + /// The array index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (BodyPart[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex + Count > array.Length) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + collection.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified body part. + /// + /// + /// Removes the specified body part. + /// + /// true if the body part was removed; otherwise false. + /// The body part. + /// + /// is null. + /// + public bool Remove (BodyPart part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return collection.Remove (part); + } + + /// + /// Get the body part at the specified index. + /// + /// + /// Gets the body part at the specified index. + /// + /// The body part at the specified index. + /// The index. + /// + /// is out of range. + /// + public BodyPart this [int index] { + get { + if (index < 0 || index >= collection.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return collection[index]; + } + } + + /// + /// Gets the index of the body part matching the specified URI. + /// + /// + /// Finds the index of the body part matching the specified URI, if it exists. + /// If the URI scheme is "cid", then matching is performed based on the Content-Id header + /// values, otherwise the Content-Location headers are used. If the provided URI is absolute and a child + /// part's Content-Location is relative, then then the child part's Content-Location URI will be combined + /// with the value of its Content-Base header, if available, otherwise it will be combined with the + /// multipart/related part's Content-Base header in order to produce an absolute URI that can be + /// compared with the provided absolute URI. + /// + /// The index of the part matching the specified URI if found; otherwise -1. + /// The URI of the body part. + /// + /// is null. + /// + public int IndexOf (Uri uri) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + bool cid = uri.IsAbsoluteUri && uri.Scheme.ToLowerInvariant () == "cid"; + + for (int index = 0; index < Count; index++) { + var bodyPart = this[index] as BodyPartBasic; + + if (bodyPart == null) + continue; + + if (uri.IsAbsoluteUri) { + if (cid) { + if (!string.IsNullOrEmpty (bodyPart.ContentId)) { + // Note: we might have a Content-Id in the form "", so attempt to decode it + var id = MimeUtils.EnumerateReferences (bodyPart.ContentId).FirstOrDefault () ?? bodyPart.ContentId; + + if (id == uri.AbsolutePath) + return index; + } + } else if (bodyPart.ContentLocation != null) { + if (!bodyPart.ContentLocation.IsAbsoluteUri) + continue; + + if (bodyPart.ContentLocation == uri) + return index; + } + } else if (bodyPart.ContentLocation == uri) { + return index; + } + } + + return -1; + } + + #region IEnumerable implementation + + /// + /// Get the body part enumerator. + /// + /// + /// Gets the body part enumerator. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return collection.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get the body part enumerator. + /// + /// + /// Gets the body part enumerator. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/src/MailKit/BodyPartMessage.cs b/src/MailKit/BodyPartMessage.cs new file mode 100644 index 0000000..51985ba --- /dev/null +++ b/src/MailKit/BodyPartMessage.cs @@ -0,0 +1,124 @@ +// +// BodyPartMessage.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +namespace MailKit { + /// + /// A message/rfc822 body part. + /// + /// + /// Represents a message/rfc822 body part. + /// + public class BodyPartMessage : BodyPartBasic + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public BodyPartMessage () + { + } + + /// + /// Gets the envelope of the message, if available. + /// + /// + /// Gets the envelope of the message, if available. + /// + /// The envelope. + public Envelope Envelope { + get; set; + } + + /// + /// Gets the body structure of the message. + /// + /// + /// Gets the body structure of the message. + /// + /// The body structure. + public BodyPart Body { + get; set; + } + + /// + /// Gets the length of the message, in lines. + /// + /// + /// Gets the length of the message, in lines. + /// + /// The number of lines. + public uint Lines { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME body part. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (BodyPartVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitBodyPartMessage (this); + } + + /// + /// Encodes the into the . + /// + /// + /// Encodes the into the . + /// + /// The string builder. + protected override void Encode (StringBuilder builder) + { + base.Encode (builder); + + builder.Append (' '); + Encode (builder, Envelope); + builder.Append (' '); + Encode (builder, Body); + builder.Append (' '); + Encode (builder, Lines); + } + } +} diff --git a/src/MailKit/BodyPartMultipart.cs b/src/MailKit/BodyPartMultipart.cs new file mode 100644 index 0000000..bcdc204 --- /dev/null +++ b/src/MailKit/BodyPartMultipart.cs @@ -0,0 +1,141 @@ +// +// BodyPartMultipart.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit; + +namespace MailKit { + /// + /// A multipart body part. + /// + /// + /// A multipart body part. + /// + public class BodyPartMultipart : BodyPart + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public BodyPartMultipart () + { + BodyParts = new BodyPartCollection (); + } + + /// + /// Gets the child body parts. + /// + /// + /// Gets the child body parts. + /// + /// The child body parts. + public BodyPartCollection BodyParts { + get; private set; + } + + /// + /// Gets the Content-Disposition of the body part, if available. + /// + /// + /// Gets the Content-Disposition of the body part, if available. + /// + /// The content disposition. + public ContentDisposition ContentDisposition { + get; set; + } + + /// + /// Gets the Content-Language of the body part, if available. + /// + /// + /// Gets the Content-Language of the body part, if available. + /// + /// The content language. + public string[] ContentLanguage { + get; set; + } + + /// + /// Gets the Content-Location of the body part, if available. + /// + /// + /// Gets the Content-Location of the body part, if available. + /// + /// The content location. + public Uri ContentLocation { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME body part. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (BodyPartVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitBodyPartMultipart (this); + } + + /// + /// Encodes the into the . + /// + /// + /// Encodes the into the . + /// + /// The string builder. + protected override void Encode (StringBuilder builder) + { + Encode (builder, BodyParts); + builder.Append (' '); + Encode (builder, ContentType.MediaSubtype); + builder.Append (' '); + Encode (builder, ContentType.Parameters); + builder.Append (' '); + Encode (builder, ContentDisposition); + builder.Append (' '); + Encode (builder, ContentLanguage); + builder.Append (' '); + Encode (builder, ContentLocation); + } + } +} diff --git a/src/MailKit/BodyPartText.cs b/src/MailKit/BodyPartText.cs new file mode 100644 index 0000000..8cc9d90 --- /dev/null +++ b/src/MailKit/BodyPartText.cs @@ -0,0 +1,123 @@ +// +// BodyPartText.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +namespace MailKit { + /// + /// A textual body part. + /// + /// + /// Represents any body part with a media type of "text". + /// + /// + /// + /// + public class BodyPartText : BodyPartBasic + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public BodyPartText () + { + } + + /// + /// Gets whether or not this text part contains plain text. + /// + /// + /// Checks whether or not the text part's Content-Type is text/plain. + /// + /// true if the text is html; otherwise, false. + public bool IsPlain { + get { return ContentType.IsMimeType ("text", "plain"); } + } + + /// + /// Gets whether or not this text part contains HTML. + /// + /// + /// Checks whether or not the text part's Content-Type is text/html. + /// + /// true if the text is html; otherwise, false. + public bool IsHtml { + get { return ContentType.IsMimeType ("text", "html"); } + } + + /// + /// Gets the length of the text, in lines. + /// + /// + /// Gets the length of the text, in lines. + /// + /// The number of lines. + public uint Lines { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME body part. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (BodyPartVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitBodyPartText (this); + } + + /// + /// Encodes the into the . + /// + /// + /// Encodes the into the . + /// + /// The string builder. + protected override void Encode (StringBuilder builder) + { + base.Encode (builder); + + builder.Append (' '); + Encode (builder, Lines); + } + } +} diff --git a/src/MailKit/BodyPartVisitor.cs b/src/MailKit/BodyPartVisitor.cs new file mode 100644 index 0000000..8c06173 --- /dev/null +++ b/src/MailKit/BodyPartVisitor.cs @@ -0,0 +1,137 @@ +// +// BodyPartVisitor.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// Represents a visitor for a tree of MIME body parts. + /// + /// + /// This class is designed to be inherited to create more specialized classes whose + /// functionality requires traversing, examining or copying a tree of MIME body parts. + /// + public abstract class BodyPartVisitor + { + /// + /// Dispatches the entity to one of the more specialized visit methods in this class. + /// + /// + /// Dispatches the entity to one of the more specialized visit methods in this class. + /// + /// The MIME body part. + public virtual void Visit (BodyPart body) + { + if (body != null) + body.Accept (this); + } + + /// + /// Visit the abstract MIME body part. + /// + /// + /// Visits the abstract MIME body part. + /// + /// The MIME body part. + protected internal virtual void VisitBodyPart (BodyPart entity) + { + } + + /// + /// Visit the basic MIME body part. + /// + /// + /// Visits the basic MIME body part. + /// + /// The basic MIME body part. + protected internal virtual void VisitBodyPartBasic (BodyPartBasic entity) + { + VisitBodyPart (entity); + } + + /// + /// Visit the message contained within a message/rfc822 or message/news MIME entity. + /// + /// + /// Visits the message contained within a message/rfc822 or message/news MIME entity. + /// + /// The body part representing the message/rfc822 message. + protected virtual void VisitMessage (BodyPart message) + { + if (message != null) + message.Accept (this); + } + + /// + /// Visit the message/rfc822 or message/news MIME entity. + /// + /// + /// Visits the message/rfc822 or message/news MIME entity. + /// + /// The message/rfc822 or message/news body part. + protected internal virtual void VisitBodyPartMessage (BodyPartMessage entity) + { + VisitBodyPartBasic (entity); + VisitMessage (entity.Body); + } + + /// + /// Visit the children of a . + /// + /// + /// Visits the children of a . + /// + /// The multipart. + protected virtual void VisitChildren (BodyPartMultipart multipart) + { + for (int i = 0; i < multipart.BodyParts.Count; i++) + multipart.BodyParts[i].Accept (this); + } + + /// + /// Visit the abstract multipart MIME entity. + /// + /// + /// Visits the abstract multipart MIME entity. + /// + /// The multipart body part. + protected internal virtual void VisitBodyPartMultipart (BodyPartMultipart multipart) + { + VisitBodyPart (multipart); + VisitChildren (multipart); + } + + /// + /// Visit the text-based MIME part entity. + /// + /// + /// Visits the text-based MIME part entity. + /// + /// The text-based body part. + protected internal virtual void VisitBodyPartText (BodyPartText entity) + { + VisitBodyPartBasic (entity); + } + } +} diff --git a/src/MailKit/CommandException.cs b/src/MailKit/CommandException.cs new file mode 100644 index 0000000..b47bbfe --- /dev/null +++ b/src/MailKit/CommandException.cs @@ -0,0 +1,102 @@ +// +// CommandException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when there is a command error. + /// + /// + /// A can be thrown by any of the various client + /// methods in MailKit. Unlike a , a + /// is typically non-fatal (meaning that it does + /// not force the client to disconnect). + /// +#if SERIALIZABLE + [Serializable] +#endif + public abstract class CommandException : Exception + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected CommandException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + protected CommandException (string message, Exception innerException) : base (message, innerException) + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + protected CommandException (string message) : base (message) + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + protected CommandException () + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + } +} diff --git a/src/MailKit/CompressedStream.cs b/src/MailKit/CompressedStream.cs new file mode 100644 index 0000000..1de9858 --- /dev/null +++ b/src/MailKit/CompressedStream.cs @@ -0,0 +1,435 @@ +// +// CompressedStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Org.BouncyCastle.Utilities.Zlib; + +namespace MailKit { + /// + /// A compressed stream. + /// + class CompressedStream : Stream + { + readonly ZStream zIn, zOut; + bool eos, disposed; + + public CompressedStream (Stream innerStream) + { + InnerStream = innerStream; + + zOut = new ZStream (); + zOut.deflateInit (5, true); + zOut.next_out = new byte[4096]; + + zIn = new ZStream (); + zIn.inflateInit (true); + zIn.next_in = new byte[4096]; + } + + /// + /// Gets the inner stream. + /// + /// The inner stream. + public Stream InnerStream { + get; private set; + } + + /// + /// Gets whether the stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return InnerStream.CanRead; } + } + + /// + /// Gets whether the stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return InnerStream.CanWrite; } + } + + /// + /// Gets whether the stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Gets whether the stream supports I/O timeouts. + /// + /// true if the stream supports I/O timeouts; otherwise, false. + public override bool CanTimeout { + get { return InnerStream.CanTimeout; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// The read timeout. + public override int ReadTimeout { + get { return InnerStream.ReadTimeout; } + set { InnerStream.ReadTimeout = value; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// The write timeout. + public override int WriteTimeout { + get { return InnerStream.WriteTimeout; } + set { InnerStream.WriteTimeout = value; } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// The position of the stream. + /// + /// The stream does not support seeking. + /// + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + /// The length of the stream. + /// + /// The stream does not support seeking. + /// + public override long Length { + get { throw new NotSupportedException (); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (CompressedStream)); + } + + async Task ReadAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (count == 0) + return 0; + + zIn.next_out = buffer; + zIn.next_out_index = offset; + zIn.avail_out = count; + + do { + if (zIn.avail_in == 0 && !eos) { + cancellationToken.ThrowIfCancellationRequested (); + + if (doAsync) + zIn.avail_in = await InnerStream.ReadAsync (zIn.next_in, 0, zIn.next_in.Length, cancellationToken).ConfigureAwait (false); + else + zIn.avail_in = InnerStream.Read (zIn.next_in, 0, zIn.next_in.Length); + + eos = zIn.avail_in == 0; + zIn.next_in_index = 0; + } + + int retval = zIn.inflate (JZlib.Z_FULL_FLUSH); + + if (retval == JZlib.Z_STREAM_END) + break; + + if (eos && retval == JZlib.Z_BUF_ERROR) + return 0; + + if (retval != JZlib.Z_OK) + throw new IOException ("Error inflating: " + zIn.msg); + } while (zIn.avail_out == count); + + return count - zIn.avail_out; + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + return ReadAsync (buffer, offset, count, false, CancellationToken.None).GetAwaiter ().GetResult (); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync (buffer, offset, count, true, cancellationToken); + } + + async Task WriteAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (count == 0) + return; + + zOut.next_in = buffer; + zOut.next_in_index = offset; + zOut.avail_in = count; + + do { + cancellationToken.ThrowIfCancellationRequested (); + + zOut.avail_out = zOut.next_out.Length; + zOut.next_out_index = 0; + + if (zOut.deflate (JZlib.Z_FULL_FLUSH) != JZlib.Z_OK) + throw new IOException ("Error deflating: " + zOut.msg); + + if (doAsync) + await InnerStream.WriteAsync (zOut.next_out, 0, zOut.next_out.Length - zOut.avail_out, cancellationToken).ConfigureAwait (false); + else + InnerStream.Write (zOut.next_out, 0, zOut.next_out.Length - zOut.avail_out); + } while (zOut.avail_in > 0 || zOut.avail_out == 0); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + WriteAsync (buffer, offset, count, false, CancellationToken.None).GetAwaiter ().GetResult (); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync (buffer, offset, count, true, cancellationToken); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + CheckDisposed (); + + InnerStream.Flush (); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + return InnerStream.FlushAsync (cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + /// + /// Sets the length of the stream. + /// + /// The desired length of the stream in bytes. + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + InnerStream.Dispose (); + disposed = true; + zOut.free (); + zIn.free (); + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/ConnectedEventArgs.cs b/src/MailKit/ConnectedEventArgs.cs new file mode 100644 index 0000000..99fc534 --- /dev/null +++ b/src/MailKit/ConnectedEventArgs.cs @@ -0,0 +1,88 @@ +// +// ConnectedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MailKit.Security; + +namespace MailKit +{ + /// + /// Connected event arguments. + /// + /// + /// When a is connected, it will emit a + /// event. + /// + public class ConnectedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the host that the client connected to. + /// The port that the client connected to on the remote host. + /// The SSL/TLS options that were used when connecting to the remote host. + public ConnectedEventArgs (string host, int port, SecureSocketOptions options) + { + Options = options; + Host = host; + Port = port; + } + + /// + /// Get the name of the remote host. + /// + /// + /// Gets the name of the remote host. + /// + /// The host name of the server. + public string Host { + get; private set; + } + + /// + /// Get the port. + /// + /// + /// Gets the port. + /// + /// The port. + public int Port { + get; private set; + } + + /// + /// Get the SSL/TLS options. + /// + /// + /// Gets the SSL/TLS options. + /// + /// The SSL/TLS options. + public SecureSocketOptions Options { + get; private set; + } + } +} diff --git a/src/MailKit/DeliveryStatusNotification.cs b/src/MailKit/DeliveryStatusNotification.cs new file mode 100644 index 0000000..72c2823 --- /dev/null +++ b/src/MailKit/DeliveryStatusNotification.cs @@ -0,0 +1,61 @@ +// +// DeliveryStatusNotification.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Delivery status notification types. + /// + /// + /// A set of flags that may be bitwise-or'd together to specify + /// when a delivery status notification should be sent for a + /// particlar recipient. + /// + [Flags] + public enum DeliveryStatusNotification { + /// + /// Never send delivery status notifications. + /// + Never = 0, + + /// + /// Send a notification on successful delivery to the recipient. + /// + Success = (1 << 0), + + /// + /// Send a notification on failure to deliver to the recipient. + /// + Failure = (1 << 1), + + /// + /// Send a notification when the delivery to the recipient has + /// been delayed for an unusual amount of time. + /// + Delay = (1 << 2) + } +} diff --git a/src/MailKit/DeliveryStatusNotificationType.cs b/src/MailKit/DeliveryStatusNotificationType.cs new file mode 100644 index 0000000..717404b --- /dev/null +++ b/src/MailKit/DeliveryStatusNotificationType.cs @@ -0,0 +1,54 @@ +// +// DeliveryStatusNotificationReturnType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2019 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Smtp +{ + /// + /// Delivery status notification type. + /// + /// + /// The delivery status notification type specifies whether or not + /// the full message should be included in any failed DSN issued for + /// a message transmission as opposed to just the headers. + /// + public enum DeliveryStatusNotificationType + { + /// + /// The return type is unspecified, allowing the server to choose. + /// + Unspecified, + + /// + /// The full message should be included in any failed delivery status notification issued by the server. + /// + Full, + + /// + /// Only the headers should be included in any failed delivery status notification issued by the server. + /// + HeadersOnly, + } +} diff --git a/src/MailKit/DisconnectedEventArgs.cs b/src/MailKit/DisconnectedEventArgs.cs new file mode 100644 index 0000000..676c904 --- /dev/null +++ b/src/MailKit/DisconnectedEventArgs.cs @@ -0,0 +1,70 @@ +// +// DisconnectedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit.Security; + +namespace MailKit +{ + /// + /// Disconnected event arguments. + /// + /// + /// When a gets disconnected, it will emit a + /// event. + /// + public class DisconnectedEventArgs : ConnectedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the host that the client was connected to. + /// The port that the client was connected to. + /// The SSL/TLS options that were used by the client. + /// If true, the was disconnected via the + /// method. + public DisconnectedEventArgs (string host, int port, SecureSocketOptions options, bool requested) : base (host, port, options) + { + IsRequested = requested; + } + + /// + /// Get whether or not the service was explicitly asked to disconnect. + /// + /// + /// If the was disconnected via the + /// method, then + /// the value of will be true. If the connection was unexpectedly + /// dropped, then the value will be false. + /// + /// true if the disconnect was explicitly requested; otherwise, false. + public bool IsRequested { + get; private set; + } + } +} diff --git a/src/MailKit/DuplexStream.cs b/src/MailKit/DuplexStream.cs new file mode 100644 index 0000000..e71ab1e --- /dev/null +++ b/src/MailKit/DuplexStream.cs @@ -0,0 +1,395 @@ +// +// DuplexStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MailKit { + /// + /// A duplex stream. + /// + class DuplexStream : Stream + { + bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to use for input. + /// The stream to use for output. + /// + /// is null. + /// -or- + /// is null. + /// + public DuplexStream (Stream istream, Stream ostream) + { + if (istream == null) + throw new ArgumentNullException (nameof (istream)); + + if (ostream == null) + throw new ArgumentNullException (nameof (ostream)); + + InputStream = istream; + OutputStream = ostream; + } + + /// + /// Gets the input stream. + /// + /// The input stream. + public Stream InputStream { + get; private set; + } + + /// + /// Gets the output stream. + /// + /// The output stream. + public Stream OutputStream { + get; private set; + } + + /// + /// Gets whether the stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return true; } + } + + /// + /// Gets whether the stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return true; } + } + + /// + /// Gets whether the stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Gets whether the stream supports I/O timeouts. + /// + /// true if the stream supports I/O timeouts; otherwise, false. + public override bool CanTimeout { + get { return InputStream.CanTimeout && OutputStream.CanTimeout; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// The read timeout. + public override int ReadTimeout { + get { return InputStream.ReadTimeout; } + set { InputStream.ReadTimeout = value; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// The write timeout. + public override int WriteTimeout { + get { return OutputStream.WriteTimeout; } + set { OutputStream.WriteTimeout = value; } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// The position of the stream. + /// + /// The stream does not support seeking. + /// + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + /// The length of the stream. + /// + /// The stream does not support seeking. + /// + public override long Length { + get { throw new NotSupportedException (); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (DuplexStream)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + return InputStream.Read (buffer, offset, count); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + return InputStream.ReadAsync (buffer, offset, count, cancellationToken); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + OutputStream.Write (buffer, offset, count); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + return OutputStream.WriteAsync (buffer, offset, count, cancellationToken); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + CheckDisposed (); + + OutputStream.Flush (); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + return OutputStream.FlushAsync (cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + /// + /// Sets the length of the stream. + /// + /// The desired length of the stream in bytes. + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + OutputStream.Dispose (); + InputStream.Dispose (); + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Envelope.cs b/src/MailKit/Envelope.cs new file mode 100644 index 0000000..76366cc --- /dev/null +++ b/src/MailKit/Envelope.cs @@ -0,0 +1,592 @@ +// +// Envelope.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.Utils; + +namespace MailKit { + /// + /// A message envelope containing a brief summary of the message. + /// + /// + /// The envelope of a message contains information such as the + /// date the message was sent, the subject of the message, + /// the sender of the message, who the message was sent to, + /// which message(s) the message may be in reply to, + /// and the message id. + /// + public class Envelope + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public Envelope () + { + From = new InternetAddressList (); + Sender = new InternetAddressList (); + ReplyTo = new InternetAddressList (); + To = new InternetAddressList (); + Cc = new InternetAddressList (); + Bcc = new InternetAddressList (); + } + + /// + /// Gets the address(es) that the message is from. + /// + /// + /// Gets the address(es) that the message is from. + /// + /// The address(es) that the message is from. + public InternetAddressList From { + get; private set; + } + + /// + /// Gets the actual sender(s) of the message. + /// + /// + /// The senders may differ from the addresses in if + /// the message was sent by someone on behalf of someone else. + /// + /// The actual sender(s) of the message. + public InternetAddressList Sender { + get; private set; + } + + /// + /// Gets the address(es) that replies should be sent to. + /// + /// + /// The senders of the message may prefer that replies are sent + /// somewhere other than the address they used to send the message. + /// + /// The address(es) that replies should be sent to. + public InternetAddressList ReplyTo { + get; private set; + } + + /// + /// Gets the list of addresses that the message was sent to. + /// + /// + /// Gets the list of addresses that the message was sent to. + /// + /// The address(es) that the message was sent to. + public InternetAddressList To { + get; private set; + } + + /// + /// Gets the list of addresses that the message was carbon-copied to. + /// + /// + /// Gets the list of addresses that the message was carbon-copied to. + /// + /// The address(es) that the message was carbon-copied to. + public InternetAddressList Cc { + get; private set; + } + + /// + /// Gets the list of addresses that the message was blind-carbon-copied to. + /// + /// + /// Gets the list of addresses that the message was blind-carbon-copied to. + /// + /// The address(es) that the message was carbon-copied to. + public InternetAddressList Bcc { + get; private set; + } + + /// + /// The Message-Id that the message is replying to. + /// + /// + /// The Message-Id that the message is replying to. + /// + /// The Message-Id that the message is replying to. + public string InReplyTo { + get; set; + } + + /// + /// Gets the date that the message was sent on, if available. + /// + /// + /// Gets the date that the message was sent on, if available. + /// + /// The date the message was sent. + public DateTimeOffset? Date { + get; set; + } + + /// + /// Gets the ID of the message, if available. + /// + /// + /// Gets the ID of the message, if available. + /// + /// The message identifier. + public string MessageId { + get; set; + } + + /// + /// Gets the subject of the message. + /// + /// + /// Gets the subject of the message. + /// + /// The subject. + public string Subject { + get; set; + } + + static void EncodeMailbox (StringBuilder builder, MailboxAddress mailbox) + { + builder.Append ('('); + + if (mailbox.Name != null) + builder.AppendFormat ("{0} ", MimeUtils.Quote (mailbox.Name)); + else + builder.Append ("NIL "); + + if (mailbox.Route.Count != 0) + builder.AppendFormat ("\"{0}\" ", mailbox.Route); + else + builder.Append ("NIL "); + + int at = mailbox.Address.LastIndexOf ('@'); + + if (at >= 0) { + var domain = mailbox.Address.Substring (at + 1); + var user = mailbox.Address.Substring (0, at); + + builder.AppendFormat ("{0} {1}", MimeUtils.Quote (user), MimeUtils.Quote (domain)); + } else { + builder.AppendFormat ("{0} \"localhost\"", MimeUtils.Quote (mailbox.Address)); + } + + builder.Append (')'); + } + + static void EncodeInternetAddressListAddresses (StringBuilder builder, InternetAddressList addresses) + { + foreach (var addr in addresses) { + var mailbox = addr as MailboxAddress; + var group = addr as GroupAddress; + + if (mailbox != null) + EncodeMailbox (builder, mailbox); + else if (group != null) + EncodeGroup (builder, group); + } + } + + static void EncodeGroup (StringBuilder builder, GroupAddress group) + { + builder.AppendFormat ("(NIL NIL {0} NIL)", MimeUtils.Quote (group.Name)); + EncodeInternetAddressListAddresses (builder, group.Members); + builder.Append ("(NIL NIL NIL NIL)"); + } + + static void EncodeAddressList (StringBuilder builder, InternetAddressList list) + { + builder.Append ('('); + EncodeInternetAddressListAddresses (builder, list); + builder.Append (')'); + } + + internal void Encode (StringBuilder builder) + { + builder.Append ('('); + + if (Date.HasValue) + builder.AppendFormat ("\"{0}\" ", DateUtils.FormatDate (Date.Value)); + else + builder.Append ("NIL "); + + if (Subject != null) + builder.AppendFormat ("{0} ", MimeUtils.Quote (Subject)); + else + builder.Append ("NIL "); + + if (From.Count > 0) { + EncodeAddressList (builder, From); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (Sender.Count > 0) { + EncodeAddressList (builder, Sender); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (ReplyTo.Count > 0) { + EncodeAddressList (builder, ReplyTo); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (To.Count > 0) { + EncodeAddressList (builder, To); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (Cc.Count > 0) { + EncodeAddressList (builder, Cc); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (Bcc.Count > 0) { + EncodeAddressList (builder, Bcc); + builder.Append (' '); + } else { + builder.Append ("NIL "); + } + + if (InReplyTo != null) { + if (InReplyTo.Length > 1 && InReplyTo[0] != '<' && InReplyTo[InReplyTo.Length - 1] != '>') + builder.AppendFormat ("{0} ", MimeUtils.Quote ('<' + InReplyTo + '>')); + else + builder.AppendFormat ("{0} ", MimeUtils.Quote (InReplyTo)); + } else + builder.Append ("NIL "); + + if (MessageId != null) { + if (MessageId.Length > 1 && MessageId[0] != '<' && MessageId[MessageId.Length - 1] != '>') + builder.AppendFormat ("{0}", MimeUtils.Quote ('<' + MessageId + '>')); + else + builder.AppendFormat ("{0}", MimeUtils.Quote (MessageId)); + } else + builder.Append ("NIL"); + + builder.Append (')'); + } + + /// + /// Returns a that represents the current . + /// + /// + /// The returned string can be parsed by . + /// The syntax of the string returned, while similar to IMAP's ENVELOPE syntax, + /// is not completely compatible. + /// + /// A that represents the current . + public override string ToString () + { + var builder = new StringBuilder (); + + Encode (builder); + + return builder.ToString (); + } + + static bool TryParse (string text, ref int index, out string nstring) + { + nstring = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '"') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + var token = new StringBuilder (); + bool escaped = false; + + index++; + + while (index < text.Length) { + if (text[index] == '"' && !escaped) + break; + + if (escaped || text[index] != '\\') { + token.Append (text[index]); + escaped = false; + } else { + escaped = true; + } + + index++; + } + + if (index >= text.Length) + return false; + + nstring = token.ToString (); + + index++; + + return true; + } + + static bool TryParse (string text, ref int index, out InternetAddress addr) + { + string name, route, user, domain; + DomainList domains; + + addr = null; + + if (text[index] != '(') + return false; + + index++; + + if (!TryParse (text, ref index, out name)) + return false; + + if (!TryParse (text, ref index, out route)) + return false; + + if (!TryParse (text, ref index, out user)) + return false; + + if (!TryParse (text, ref index, out domain)) + return false; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length || text[index] != ')') + return false; + + index++; + + if (domain != null) { + var address = user + "@" + domain; + + if (route != null && DomainList.TryParse (route, out domains)) + addr = new MailboxAddress (name, domains, address); + else + addr = new MailboxAddress (name, address); + } else if (user != null) { + addr = new GroupAddress (user); + } + + return true; + } + + static bool TryParse (string text, ref int index, out InternetAddressList list) + { + list = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length) + return false; + + if (text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + list = new InternetAddressList (); + index += 3; + return true; + } + + return false; + } + + index++; + + if (index >= text.Length) + return false; + + list = new InternetAddressList (); + var stack = new List (); + int sp = 0; + + stack.Add (list); + + do { + if (text[index] == ')') + break; + + if (!TryParse (text, ref index, out InternetAddress addr)) + return false; + + if (addr != null) { + var group = addr as GroupAddress; + + stack[sp].Add (addr); + + if (group != null) { + stack.Add (group.Members); + sp++; + } + } else if (sp > 0) { + stack.RemoveAt (sp); + sp--; + } + + while (index < text.Length && text[index] == ' ') + index++; + } while (index < text.Length); + + // Note: technically, we should check that sp == 0 as well, since all groups should + // be popped off the stack, but in the interest of being liberal in what we accept, + // we'll ignore that. + if (index >= text.Length) + return false; + + index++; + + return true; + } + + internal static bool TryParse (string text, ref int index, out Envelope envelope) + { + InternetAddressList from, sender, replyto, to, cc, bcc; + string inreplyto, messageid, subject, nstring; + DateTimeOffset? date = null; + + envelope = null; + + while (index < text.Length && text[index] == ' ') + index++; + + if (index >= text.Length || text[index] != '(') { + if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { + index += 3; + return true; + } + + return false; + } + + index++; + + if (!TryParse (text, ref index, out nstring)) + return false; + + if (nstring != null) { + DateTimeOffset value; + + if (!DateUtils.TryParse (nstring, out value)) + return false; + + date = value; + } + + if (!TryParse (text, ref index, out subject)) + return false; + + if (!TryParse (text, ref index, out from)) + return false; + + if (!TryParse (text, ref index, out sender)) + return false; + + if (!TryParse (text, ref index, out replyto)) + return false; + + if (!TryParse (text, ref index, out to)) + return false; + + if (!TryParse (text, ref index, out cc)) + return false; + + if (!TryParse (text, ref index, out bcc)) + return false; + + if (!TryParse (text, ref index, out inreplyto)) + return false; + + if (!TryParse (text, ref index, out messageid)) + return false; + + if (index >= text.Length || text[index] != ')') + return false; + + index++; + + envelope = new Envelope { + Date = date, + Subject = subject, + From = from, + Sender = sender, + ReplyTo = replyto, + To = to, + Cc = cc, + Bcc = bcc, + InReplyTo = inreplyto != null ? MimeUtils.EnumerateReferences (inreplyto).FirstOrDefault () ?? inreplyto : null, + MessageId = messageid != null ? MimeUtils.EnumerateReferences (messageid).FirstOrDefault () ?? messageid : null + }; + + return true; + } + + /// + /// Tries to parse the given text into a new instance. + /// + /// + /// Parses an Envelope value from the specified text. + /// This syntax, while similar to IMAP's ENVELOPE syntax, is not + /// completely compatible. + /// + /// true, if the envelope was successfully parsed, false otherwise. + /// The text to parse. + /// The parsed envelope. + /// + /// is null. + /// + public static bool TryParse (string text, out Envelope envelope) + { + if (text == null) + throw new ArgumentNullException (nameof (text)); + + int index = 0; + + return TryParse (text, ref index, out envelope) && index == text.Length; + } + } +} + \ No newline at end of file diff --git a/src/MailKit/FolderAccess.cs b/src/MailKit/FolderAccess.cs new file mode 100644 index 0000000..940b4e0 --- /dev/null +++ b/src/MailKit/FolderAccess.cs @@ -0,0 +1,53 @@ +// +// FolderMode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// A folder access mode. + /// + /// + /// A folder access mode. + /// + /// + /// + /// + public enum FolderAccess { + /// + /// The folder is not open. + /// + None, + + /// + /// The folder is read-only. + /// + ReadOnly, + + /// + /// The folder is read/write. + /// + ReadWrite + } +} diff --git a/src/MailKit/FolderAttributes.cs b/src/MailKit/FolderAttributes.cs new file mode 100644 index 0000000..e87ebca --- /dev/null +++ b/src/MailKit/FolderAttributes.cs @@ -0,0 +1,135 @@ +// +// FolderAttributes.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Folder attributes as used by . + /// + /// + /// Folder attributes as used by . + /// + [Flags] + public enum FolderAttributes { + /// + /// The folder does not have any attributes. + /// + None = 0, + + /// + /// It is not possible for any subfolders to exist under the folder. + /// + NoInferiors = (1 << 0), + + /// + /// It is not possible to select the folder. + /// + NoSelect = (1 << 1), + + /// + /// The folder has been marked as possibly containing new messages + /// since the folder was last selected. + /// + Marked = (1 << 2), + + /// + /// The folder does not contain any new messages since the folder + /// was last selected. + /// + Unmarked = (1 << 3), + + /// + /// The folder does not exist, but is simply a place-holder. + /// + NonExistent = (1 << 4), + + /// + /// The folder is subscribed. + /// + Subscribed = (1 << 5), + + /// + /// The folder is remote. + /// + Remote = (1 << 6), + + /// + /// The folder has subfolders. + /// + HasChildren = (1 << 7), + + /// + /// The folder does not have any subfolders. + /// + HasNoChildren = (1 << 8), + + /// + /// The folder is a special "All" folder containing an aggregate of all messages. + /// + All = (1 << 9), + + /// + /// The folder is a special "Archive" folder. + /// + Archive = (1 << 10), + + /// + /// The folder is the special "Drafts" folder. + /// + Drafts = (1 << 11), + + /// + /// The folder is the special "Flagged" folder. + /// + Flagged = (1 << 12), + + /// + /// The folder is the special "Important" folder. + /// + Important = (1 << 13), + + /// + /// The folder is the special "Inbox" folder. + /// + Inbox = (1 << 14), + + /// + /// The folder is the special "Junk" folder. + /// + Junk = (1 << 15), + + /// + /// The folder is the special "Sent" folder. + /// + Sent = (1 << 16), + + /// + /// The folder is the special "Trash" folder. + /// + Trash = (1 << 17), + } +} diff --git a/src/MailKit/FolderCreatedEventArgs.cs b/src/MailKit/FolderCreatedEventArgs.cs new file mode 100644 index 0000000..949a1ad --- /dev/null +++ b/src/MailKit/FolderCreatedEventArgs.cs @@ -0,0 +1,67 @@ +// +// FolderCreatedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a is created. + /// + /// + /// Event args used when a is created. + /// + public class FolderCreatedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The newly created folder. + /// + /// is null. + /// + public FolderCreatedEventArgs (IMailFolder folder) + { + if (folder == null) + throw new ArgumentNullException (nameof (folder)); + + Folder = folder; + } + + /// + /// Get the folder that was just created. + /// + /// + /// Gets the folder that was just created. + /// + /// The folder. + public IMailFolder Folder { + get; private set; + } + } +} diff --git a/src/MailKit/FolderFeature.cs b/src/MailKit/FolderFeature.cs new file mode 100644 index 0000000..f9774fa --- /dev/null +++ b/src/MailKit/FolderFeature.cs @@ -0,0 +1,82 @@ +// +// FolderFeature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit +{ + /// + /// An optional feature that an may support. + /// + /// + /// An optional feature that an may support. + /// + public enum FolderFeature + { + /// + /// Indicates that the folder supports access rights. + /// + AccessRights, + + /// + /// Indicates that the folder allows arbitrary annotations to be set on a message. + /// + Annotations, + + /// + /// Indicates that the folder allows arbitrary metadata to be set. + /// + Metadata, + + /// + /// Indicates that the folder uses modification sequences for every state change of a message. + /// + ModSequences, + + /// + /// Indicates that the folder supports quick resynchronization when opening. + /// + QuickResync, + + /// + /// Indicates that the folder supports quotas. + /// + Quotas, + + /// + /// Indicates that the folder supports sorting messages. + /// + Sorting, + + /// + /// Indicates that the folder supports threading messages. + /// + Threading, + + /// + /// Indicates that the folder supports the use of UTF-8. + /// + UTF8, + } +} diff --git a/src/MailKit/FolderNamespace.cs b/src/MailKit/FolderNamespace.cs new file mode 100644 index 0000000..b1cb46e --- /dev/null +++ b/src/MailKit/FolderNamespace.cs @@ -0,0 +1,74 @@ +// +// FolderNamespace.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// A folder namespace. + /// + /// + /// A folder namespace. + /// + public class FolderNamespace + { + /// + /// The directory separator for this folder namespace. + /// + /// + /// The directory separator for this folder namespace. + /// + public readonly char DirectorySeparator; + + /// + /// The base path for this folder namespace. + /// + /// + /// The base path for this folder namespace. + /// + public readonly string Path; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new folder namespace. + /// + /// The directory separator. + /// The folder path. + /// + /// is null. + /// + public FolderNamespace (char directorySeparator, string path) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + DirectorySeparator = directorySeparator; + Path = path; + } + } +} diff --git a/src/MailKit/FolderNamespaceCollection.cs b/src/MailKit/FolderNamespaceCollection.cs new file mode 100644 index 0000000..c432b39 --- /dev/null +++ b/src/MailKit/FolderNamespaceCollection.cs @@ -0,0 +1,235 @@ +// +// FolderNamespaceCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MailKit { + /// + /// A read-only collection of folder namespaces. + /// + /// + /// A read-only collection of folder namespaces. + /// + public class FolderNamespaceCollection : IEnumerable + { + readonly List namespaces; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public FolderNamespaceCollection () + { + namespaces = new List (); + } + + #region ICollection implementation + + /// + /// Gets the number of folder namespaces contained in the collection. + /// + /// + /// Gets the number of folder namespaces contained in the collection. + /// + /// The count. + public int Count { + get { return namespaces.Count; } + } + + /// + /// Adds the specified namespace. + /// + /// + /// Adds the specified namespace. + /// + /// The namespace to add. + /// + /// is null. + /// + public void Add (FolderNamespace @namespace) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + namespaces.Add (@namespace); + } + + /// + /// Removes all namespaces from the collection. + /// + /// + /// Removes all namespaces from the collection. + /// + public void Clear () + { + namespaces.Clear (); + } + + /// + /// Checks if the collection contains the specified namespace. + /// + /// + /// Checks if the collection contains the specified namespace. + /// + /// true if the specified namespace exists; + /// otherwise false. + /// The namespace. + /// + /// is null. + /// + public bool Contains (FolderNamespace @namespace) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + return namespaces.Contains (@namespace); + } + + /// + /// Removes the first occurance of the specified namespace. + /// + /// + /// Removes the first occurance of the specified namespace. + /// + /// true if the frst occurance of the specified + /// namespace was removed; otherwise false. + /// The namespace. + /// + /// is null. + /// + public bool Remove (FolderNamespace @namespace) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + return namespaces.Remove (@namespace); + } + + /// + /// Gets the at the specified index. + /// + /// + /// Gets the at the specified index. + /// + /// The folder namespace at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public FolderNamespace this [int index] { + get { + if (index < 0 || index >= namespaces.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return namespaces[index]; + } + set { + if (index < 0 || index >= namespaces.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + namespaces[index] = value; + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets the enumerator. + /// + /// + /// Gets the enumerator. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return namespaces.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets the enumerator. + /// + /// + /// Gets the enumerator. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return namespaces.GetEnumerator (); + } + + #endregion + + static bool Escape (char directorySeparator) + { + return directorySeparator == '\\' || directorySeparator == '"'; + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + var builder = new StringBuilder (); + + builder.Append ('('); + for (int i = 0; i < namespaces.Count; i++) { + builder.Append ("(\""); + if (Escape (namespaces[i].DirectorySeparator)) + builder.Append ('\\'); + builder.Append (namespaces[i].DirectorySeparator); + builder.Append ("\" "); + builder.Append (MimeUtils.Quote (namespaces[i].Path)); + builder.Append (")"); + } + builder.Append (')'); + + return builder.ToString (); + } + } +} diff --git a/src/MailKit/FolderNotFoundException.cs b/src/MailKit/FolderNotFoundException.cs new file mode 100644 index 0000000..9747e95 --- /dev/null +++ b/src/MailKit/FolderNotFoundException.cs @@ -0,0 +1,149 @@ +// +// FolderNotFoundException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when a folder could not be found. + /// + /// + /// This exception is thrown by . + /// +#if SERIALIZABLE + [Serializable] +#endif + public class FolderNotFoundException : Exception + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + protected FolderNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) + { + FolderName = info.GetString ("FolderName"); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The name of the folder. + /// The inner exception. + /// + /// is null. + /// + public FolderNotFoundException (string message, string folderName, Exception innerException) : base (message, innerException) + { + if (folderName == null) + throw new ArgumentNullException (nameof (folderName)); + + FolderName = folderName; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The name of the folder. + /// + /// is null. + /// + public FolderNotFoundException (string message, string folderName) : base (message) + { + if (folderName == null) + throw new ArgumentNullException (nameof (folderName)); + + FolderName = folderName; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The name of the folder. + /// + /// is null. + /// + public FolderNotFoundException (string folderName) : this ("The requested folder could not be found.", folderName) + { + } + + /// + /// Gets the name of the folder that could not be found. + /// + /// + /// Gets the name of the folder that could not be found. + /// + /// The name of the folder. + public string FolderName { + get; private set; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("FolderName", FolderName); + } +#endif + } +} diff --git a/src/MailKit/FolderNotOpenException.cs b/src/MailKit/FolderNotOpenException.cs new file mode 100644 index 0000000..1a52d99 --- /dev/null +++ b/src/MailKit/FolderNotOpenException.cs @@ -0,0 +1,184 @@ +// +// FolderNotOpenException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when a folder is not open. + /// + /// + /// This exception is thrown when an operation on a folder could not be completed + /// due to the folder being in a closed state. For example, the + /// + /// method will throw a if the folder is not + /// current open. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class FolderNotOpenException : InvalidOperationException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + protected FolderNotOpenException (SerializationInfo info, StreamingContext context) : base (info, context) + { + var value = info.GetString ("FolderAccess"); + FolderAccess access; + + if (!Enum.TryParse (value, out access)) + FolderAccess = FolderAccess.ReadOnly; + else + FolderAccess = access; + + FolderName = info.GetString ("FolderName"); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The folder name. + /// The minimum folder access required by the operation. + /// The error message. + /// The inner exception. + /// + /// is null. + /// + public FolderNotOpenException (string folderName, FolderAccess access, string message, Exception innerException) : base (message, innerException) + { + if (folderName == null) + throw new ArgumentNullException (nameof (folderName)); + + FolderName = folderName; + FolderAccess = access; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The folder name. + /// The minimum folder access required by the operation. + /// The error message. + /// + /// is null. + /// + public FolderNotOpenException (string folderName, FolderAccess access, string message) : base (message) + { + if (folderName == null) + throw new ArgumentNullException (nameof (folderName)); + + FolderName = folderName; + FolderAccess = access; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The folder name. + /// The minimum folder access required by the operation. + /// + /// is null. + /// + public FolderNotOpenException (string folderName, FolderAccess access) : this (folderName, access, GetDefaultMessage (access)) + { + } + + /// + /// Get the name of the folder. + /// + /// + /// Gets the name of the folder. + /// + /// The name of the folder. + public string FolderName { + get; private set; + } + + /// + /// Get the minimum folder access required by the operation. + /// + /// + /// Gets the minimum folder access required by the operation. + /// + /// The minimum required folder access. + public FolderAccess FolderAccess { + get; private set; + } + + static string GetDefaultMessage (FolderAccess access) + { + if (access == FolderAccess.ReadWrite) + return "The folder is not currently open in read-write mode."; + + return "The folder is not currently open."; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("FolderAccess", FolderAccess.ToString ()); + info.AddValue ("FolderName", FolderName); + } +#endif + } +} diff --git a/src/MailKit/FolderQuota.cs b/src/MailKit/FolderQuota.cs new file mode 100644 index 0000000..056fe57 --- /dev/null +++ b/src/MailKit/FolderQuota.cs @@ -0,0 +1,124 @@ +// +// FolderQuota.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// A folder quota. + /// + /// + /// A is returned by . + /// + /// + /// + /// + public class FolderQuota + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new with the specified root. + /// + /// The quota root. + public FolderQuota (IMailFolder quotaRoot) + { + QuotaRoot = quotaRoot; + } + + /// + /// Get the quota root. + /// + /// + /// Gets the quota root. If the quota root is null, then + /// it suggests that the folder does not have a quota. + /// + /// + /// + /// + /// The quota root. + public IMailFolder QuotaRoot { + get; private set; + } + + /// + /// Get or set the message limit. + /// + /// + /// Gets or sets the message limit. + /// + /// + /// + /// + /// The message limit. + public uint? MessageLimit { + get; set; + } + + /// + /// Get or set the storage limit, in kilobytes. + /// + /// + /// Gets or sets the storage limit, in kilobytes. + /// + /// + /// + /// + /// The storage limit, in kilobytes. + public uint? StorageLimit { + get; set; + } + + /// + /// Get or set the current message count. + /// + /// + /// Gets or sets the current message count. + /// + /// + /// + /// + /// The current message count. + public uint? CurrentMessageCount { + get; set; + } + + /// + /// Gets or sets the size of the current storage, in kilobytes. + /// + /// + /// Gets or sets the size of the current storage, in kilobytes. + /// + /// + /// + /// + /// The size of the current storage, in kilobytes. + public uint? CurrentStorageSize { + get; set; + } + } +} diff --git a/src/MailKit/FolderRenamedEventArgs.cs b/src/MailKit/FolderRenamedEventArgs.cs new file mode 100644 index 0000000..e83b6a1 --- /dev/null +++ b/src/MailKit/FolderRenamedEventArgs.cs @@ -0,0 +1,85 @@ +// +// FolderRenamedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a is renamed. + /// + /// + /// Event args used when a is renamed. + /// + public class FolderRenamedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The old name of the folder. + /// The new name of the folder. + /// + /// is null. + /// -or- + /// is null. + /// + public FolderRenamedEventArgs (string oldName, string newName) + { + if (oldName == null) + throw new ArgumentNullException (nameof (oldName)); + + if (newName == null) + throw new ArgumentNullException (nameof (newName)); + + OldName = oldName; + NewName = newName; + } + + /// + /// The old name of the folder. + /// + /// + /// The old name of the folder. + /// + /// The old name. + public string OldName { + get; private set; + } + + /// + /// The new name of the folder. + /// + /// + /// The new name of the folder. + /// + /// The new name. + public string NewName { + get; private set; + } + } +} diff --git a/src/MailKit/IMailFolder.cs b/src/MailKit/IMailFolder.cs new file mode 100644 index 0000000..b0065c7 --- /dev/null +++ b/src/MailKit/IMailFolder.cs @@ -0,0 +1,5077 @@ +// +// IMailFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; +using MailKit.Search; + +namespace MailKit { + /// + /// An interface for a mailbox folder as used by . + /// + /// + /// Implemented by message stores such as + /// + public interface IMailFolder : IEnumerable + { + /// + /// Gets an object that can be used to synchronize access to the folder. + /// + /// + /// Gets an object that can be used to synchronize access to the folder. + /// + /// The sync root. + object SyncRoot { get; } + + /// + /// Get the parent folder. + /// + /// + /// Root-level folders do not have a parent folder. + /// + /// The parent folder. + IMailFolder ParentFolder { get; } + + /// + /// Get the folder attributes. + /// + /// + /// Gets the folder attributes. + /// + /// The folder attributes. + FolderAttributes Attributes { get; } + + /// + /// Get the annotation access level. + /// + /// + /// If annotations are supported, this property can be used to determine whether or not + /// the supports reading and writing annotations. + /// + AnnotationAccess AnnotationAccess { get; } + + /// + /// Get the supported annotation scopes. + /// + /// + /// If annotations are supported, this property can be used to determine which + /// annotation scopes are supported by the . + /// + AnnotationScope AnnotationScopes { get; } + + /// + /// Get the maximum size of annotation values supported by the folder. + /// + /// + /// If annotations are supported, this property can be used to determine the + /// maximum size of annotation values supported by the . + /// + uint MaxAnnotationSize { get; } + + /// + /// Get the permanent flags. + /// + /// + /// The permanent flags are the message flags that will persist between sessions. + /// If the flag is set, then the folder allows + /// storing of user-defined (custom) message flags. + /// + /// The permanent flags. + MessageFlags PermanentFlags { get; } + + /// + /// Get the accepted flags. + /// + /// + /// The accepted flags are the message flags that will be accepted and persist + /// for the current session. For the set of flags that will persist between + /// sessions, see the property. + /// + /// The accepted flags. + MessageFlags AcceptedFlags { get; } + + /// + /// Get the directory separator. + /// + /// + /// Gets the directory separator. + /// + /// The directory separator. + char DirectorySeparator { get; } + + /// + /// Get the read/write access of the folder. + /// + /// + /// Gets the read/write access of the folder. + /// + /// The read/write access. + FolderAccess Access { get; } + + /// + /// Get whether or not the folder is a namespace folder. + /// + /// + /// Gets whether or not the folder is a namespace folder. + /// + /// true if the folder is a namespace folder; otherwise, false. + bool IsNamespace { get; } + + /// + /// Get the full name of the folder. + /// + /// + /// This is the equivalent of the full path of a file on a file system. + /// + /// The full name of the folder. + string FullName { get; } + + /// + /// Get the name of the folder. + /// + /// + /// This is the equivalent of the file name of a file on the file system. + /// + /// The name of the folder. + string Name { get; } + + /// + /// Get the unique identifier for the folder, if available. + /// + /// + /// Gets a unique identifier for the folder, if available. This is useful for clients + /// implementing a message cache that want to track the folder after it is renamed by another + /// client. + /// This property will only be available if the server supports the + /// OBJECTID extension. + /// + /// The unique folder identifier. + string Id { get; } + + /// + /// Get whether or not the folder is subscribed. + /// + /// + /// Gets whether or not the folder is subscribed. + /// + /// true if the folder is subscribed; otherwise, false. + bool IsSubscribed { get; } + + /// + /// Get whether or not the folder is currently open. + /// + /// + /// Gets whether or not the folder is currently open. + /// + /// true if the folder is currently open; otherwise, false. + bool IsOpen { get; } + + /// + /// Get whether or not the folder exists. + /// + /// + /// Gets whether or not the folder exists. + /// + /// true if the folder exists; otherwise, false. + bool Exists { get; } + + /// + /// Get whether or not the folder supports mod-sequences. + /// + /// + /// Gets whether or not the folder supports mod-sequences. + /// If mod-sequences are not supported by the folder, then all of the APIs that take a modseq + /// argument will throw and should not be used. + /// + /// true if the folder supports mod-sequences; otherwise, false. + [Obsolete ("Use Supports(FolderFeature.ModSequences) instead.")] + bool SupportsModSeq { get; } + + /// + /// Get the highest mod-sequence value of all messages in the mailbox. + /// + /// + /// Gets the highest mod-sequence value of all messages in the mailbox. + /// + /// The highest mod-sequence value. + ulong HighestModSeq { get; } + + /// + /// Get the Unique ID validity. + /// + /// + /// UIDs are only valid so long as the UID validity value remains unchanged. If and when + /// the folder's is changed, a client MUST discard its cache of UIDs + /// along with any summary information that it may have and re-query the folder. + /// The will only be set after the folder has been opened. + /// + /// The UID validity. + uint UidValidity { get; } + + /// + /// Get the UID that the next message that is added to the folder will be assigned. + /// + /// + /// This value will only be set after the folder has been opened. + /// + /// The next UID. + UniqueId? UidNext { get; } + + /// + /// Get the maximum size of a message that can be appended to the folder. + /// + /// + /// Gets the maximum size of a message that can be appended to the folder. + /// If the value is not set, then the limit is unspecified. + /// + /// The append limit. + uint? AppendLimit { get; } + + /// + /// Get the size of the folder. + /// + /// + /// Gets the size of the folder in bytes. + /// If the value is not set, then the size is unspecified. + /// + /// The size. + ulong? Size { get; } + + /// + /// Get the index of the first unread message in the folder. + /// + /// + /// This value will only be set after the folder has been opened. + /// + /// The index of the first unread message. + int FirstUnread { get; } + + /// + /// Get the number of unread messages in the folder. + /// + /// + /// Gets the number of unread messages in the folder. + /// This value will only be set after calling + /// + /// with . + /// + /// The number of unread messages. + int Unread { get; } + + /// + /// Get the number of recently delivered messages in the folder. + /// + /// + /// Gets the number of recently delivered messages in the folder. + /// + /// This value will only be set after calling + /// + /// with . + /// + /// The number of recently delivered messages. + int Recent { get; } + + /// + /// Get the total number of messages in the folder. + /// + /// + /// Gets the total number of messages in the folder. + /// + /// The total number of messages. + int Count { get; } + + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Get the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + HashSet ThreadingAlgorithms { get; } + + /// + /// Determine whether or not an supports a feature. + /// + /// + /// Determines whether or not an supports a feature. + /// + /// The desired feature. + /// true if the feature is supported; otherwise, false. + bool Supports (FolderFeature feature); + + /// + /// Opens the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously opens the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Open the folder using the requested folder access. + /// + /// + /// Opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously open the folder using the requested folder access. + /// + /// + /// Asynchronously opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Closes the folder, optionally expunging the messages marked for deletion. + /// + /// If set to true, expunge. + /// The cancellation token. + void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Asynchronously closes the folder, optionally expunging the messages marked for deletion. + /// + /// An asynchronous task context. + /// If set to true, expunge. + /// The cancellation token. + Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// The special use for the folder being created. + /// The cancellation token. + IMailFolder Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// The special use for the folder being created. + /// The cancellation token. + Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Rename the folder. + /// + /// + /// Renames the folder. + /// + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously rename the folder. + /// + /// + /// Asynchronously renames the folder. + /// + /// An asynchronous task context. + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Delete the folder. + /// + /// + /// Deletes the folder. + /// + /// The cancellation token. + void Delete (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously delete the folder. + /// + /// + /// Asynchronously deletes the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + Task DeleteAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Subscribe to the folder. + /// + /// + /// Subscribes to the folder. + /// + /// The cancellation token. + void Subscribe (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously subscribe to the folder. + /// + /// + /// Asynchronously subscribes to the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + Task SubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Unsubscribe from the folder. + /// + /// + /// Unsubscribes from the folder. + /// + /// The cancellation token. + void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously unsubscribe from the folder. + /// + /// + /// Asynchronously unsubscribes from the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + Task UnsubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the subfolders. + /// + /// + /// Gets the subfolders as well as queries the server for the status of the requested items. + /// When the argument is non-empty, this has the equivalent functionality + /// of calling and then calling + /// on each of the returned folders. + /// Using this method is potentially more efficient than querying the status of each returned folder. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the subfolders. + /// + /// + /// Asynchronously gets the subfolders as well as queries the server for the status of the requested items. + /// When the argument is non-empty, this has the equivalent functionality + /// of calling and then calling + /// on each of the returned folders. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the subfolders. + /// + /// + /// Gets the subfolders. + /// + /// The subfolders. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + IList GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the subfolders. + /// + /// + /// Asynchronously gets the subfolders. + /// + /// The subfolders. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified subfolder. + /// + /// + /// Gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the specified subfolder. + /// + /// + /// Asynchronously gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Force the server to flush its state for the folder. + /// + /// + /// Forces the server to flush its state for the folder. + /// + /// The cancellation token. + void Check (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously force the server to flush its state for the folder. + /// + /// + /// Asynchronously forces the server to flush its state for the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + Task CheckAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// + /// The items to update. + /// The cancellation token. + void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// + /// An asynchronous task context. + /// The items to update. + /// The cancellation token. + Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the complete access control list for the folder. + /// + /// + /// Gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the complete access control list for the folder. + /// + /// + /// Asynchronously gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + Task GetAccessControlListAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the access rights for a particular identifier. + /// + /// + /// Gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the access rights for a particular identifier. + /// + /// + /// Asynchronously gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the access rights for the current authenticated user. + /// + /// + /// Gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the access rights for the current authenticated user. + /// + /// + /// Asynchronously gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add access rights for the specified identity. + /// + /// + /// Adds the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add access rights for the specified identity. + /// + /// + /// Asynchronously adds the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove access rights for the specified identity. + /// + /// + /// Removes the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove access rights for the specified identity. + /// + /// + /// Asynchronously removes the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the access rights for the specified identity. + /// + /// + /// Sets the access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the access rights for the sepcified identity. + /// + /// + /// Asynchronously sets the access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove all access rights for the given identity. + /// + /// + /// Removes all access rights for the given identity. + /// + /// The identity name. + /// The cancellation token. + void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove all access rights for the given identity. + /// + /// + /// Asynchronously removes all access rights for the given identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The cancellation token. + Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the quota information for the folder. + /// + /// + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the quota information for the folder. + /// + /// + /// Asynchronously gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + Task GetQuotaAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the quota limits for the folder. + /// + /// + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The updated folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the quota limits for the folder. + /// + /// + /// Asynchronously sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The updated folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sets the specified metadata. + /// + /// + /// Asynchronously sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// Expunges the folder, permanently removing all messages marked for deletion. + /// Normally, an event will be emitted for each + /// message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// The cancellation token. + void Expunge (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// Asynchronously expunges the folder, permanently removing all messages marked for deletion. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// An asynchronous task context. + /// The cancellation token. + Task ExpungeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Expunges the specified uids, permanently removing them from the folder. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// The message uids. + /// The cancellation token. + void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Asynchronously expunges the specified uids, permanently removing them from the folder. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// An asynchronous task context. + /// The message uids. + /// The cancellation token. + Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + IList Append (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + Task> AppendAsync (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + IList Append (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + Task> AppendAsync (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + Task> AppendAsync (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism.; + Task ReplaceAsync (int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Copy the specified message to the destination folder. + /// + /// + /// Copies the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to copy. + /// The destination folder. + /// The cancellation token. + UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified message to the destination folder. + /// + /// + /// Asynchronously copies the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to copy. + /// The destination folder. + /// The cancellation token. + Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Asynchronously copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified message to the destination folder. + /// + /// + /// Moves the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to move. + /// The destination folder. + /// The cancellation token. + UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified message to the destination folder. + /// + /// + /// Asynchronously moves the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to move. + /// The destination folder. + /// The cancellation token. + Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// Asynchronously moves the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Copy the specified message to the destination folder. + /// + /// + /// Copies the specified message to the destination folder. + /// + /// The index of the message to copy. + /// The destination folder. + /// The cancellation token. + void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified message to the destination folder. + /// + /// + /// Asynchronously copies the specified message to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the message to copy. + /// The destination folder. + /// The cancellation token. + Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Asynchronously copies the specified messages to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified message to the destination folder. + /// + /// + /// Moves the specified message to the destination folder. + /// + /// The index of the message to move. + /// The destination folder. + /// The cancellation token. + void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified message to the destination folder. + /// + /// + /// Asynchronously moves the specified message to the destination folder. + /// + /// An asynchronous task context. + /// The index of the message to move. + /// The destination folder. + /// The cancellation token. + Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// Asynchronously moves the specified messages to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Asynchronously gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Asynchronously gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Asynchronously gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Asynchronously gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message. + /// + /// + /// Asynchronously gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message. + /// + /// + /// Asynchronously gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Asynchronously gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Asynchronously gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The UID of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The UIDs of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The UID of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The UIDs of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The UID of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The UID of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The UID of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The UID of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The index of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The index of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The indexes of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The index of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The indexes of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The index of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The indexes of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The UID of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The UIDs of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The UID of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The UID of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The index of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task AddLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The index of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task RemoveLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The index of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task SetLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> AddLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> RemoveLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + Task> SetLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + void Store (UniqueId uid, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + Task StoreAsync (UniqueId uid, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + void Store (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The index of the message. + /// The annotations to store. + /// The cancellation token. + void Store (int index, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The indexes of the message. + /// The annotations to store. + /// The cancellation token. + Task StoreAsync (int index, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// Searches the fsubset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// Asynchronously searches the fsubset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, + /// returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when the folder is opened. + /// + /// + /// Emitted when the folder is opened. + /// + event EventHandler Opened; + + /// + /// Occurs when the folder is closed. + /// + /// + /// Emitted when the folder is closed. + /// + event EventHandler Closed; + + /// + /// Occurs when the folder is deleted. + /// + /// + /// Emitted when the folder is deleted. + /// + event EventHandler Deleted; + + /// + /// Occurs when the folder is renamed. + /// + /// + /// Emitted when the folder is renamed. + /// + event EventHandler Renamed; + + /// + /// Occurs when the folder is subscribed. + /// + /// + /// Emitted when the folder is subscribed. + /// + event EventHandler Subscribed; + + /// + /// Occurs when the folder is unsubscribed. + /// + /// + /// Emitted when the folder is unsubscribed. + /// + event EventHandler Unsubscribed; + + /// + /// Occurs when a message is expunged from the folder. + /// + /// + /// Emitted when a message is expunged from the folder. + /// + /// + /// + /// + event EventHandler MessageExpunged; + + /// + /// Occurs when messages vanish from the folder. + /// + /// + /// Emitted when a messages vanish from the folder. + /// + event EventHandler MessagesVanished; + + /// + /// Occurs when flags changed on a message. + /// + /// + /// Emitted when flags changed on a message. + /// + /// + /// + /// + event EventHandler MessageFlagsChanged; + + /// + /// Occurs when labels changed on a message. + /// + /// + /// Emitted when labels changed on a message. + /// + event EventHandler MessageLabelsChanged; + + /// + /// Occurs when annotations changed on a message. + /// + /// + /// Emitted when annotations changed on a message. + /// + event EventHandler AnnotationsChanged; + + /// + /// Occurs when a message summary is fetched from the folder. + /// + /// + /// Emitted when a message summary is fetched from the folder. + /// When multiple message summaries are being fetched from a remote folder, + /// it is possible that the connection will drop or some other exception will + /// occur, causing the Fetch method to fail, requiring the client to request the + /// same set of message summaries again after it reconnects. This is obviously + /// inefficient. To alleviate this potential problem, this event will be emitted + /// as soon as the successfully retrieves the complete + /// for each requested message. + /// The Fetch + /// methods will return a list of all message summaries that any information was + /// retrieved for, regardless of whether or not all of the requested items were fetched, + /// therefore there may be a discrepency between the number of times this event is + /// emitetd and the number of summary items returned from the Fetch method. + /// + event EventHandler MessageSummaryFetched; + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + event EventHandler MetadataChanged; + + /// + /// Occurs when the mod-sequence changed on a message. + /// + /// + /// Emitted when the mod-sequence changed on a message. + /// + event EventHandler ModSeqChanged; + + /// + /// Occurs when the highest mod-sequence changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + event EventHandler HighestModSeqChanged; + + /// + /// Occurs when the next UID changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler UidNextChanged; + + /// + /// Occurs when the UID validity changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler UidValidityChanged; + + /// + /// Occurs when the ID changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler IdChanged; + + /// + /// Occurs when the size of the folder changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler SizeChanged; + + /// + /// Occurs when the message count changes. + /// + /// + /// Emitted when the property changes. + /// + /// + /// + /// + event EventHandler CountChanged; + + /// + /// Occurs when the recent message count changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler RecentChanged; + + /// + /// Occurs when the message unread count changes. + /// + /// + /// Emitted when the property changes. + /// + event EventHandler UnreadChanged; + } +} diff --git a/src/MailKit/IMailService.cs b/src/MailKit/IMailService.cs new file mode 100644 index 0000000..d350f49 --- /dev/null +++ b/src/MailKit/IMailService.cs @@ -0,0 +1,1118 @@ +// +// IMailService.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using SslProtocols = System.Security.Authentication.SslProtocols; + +using MailKit.Net.Proxy; + +using MailKit.Security; + +namespace MailKit { + /// + /// An interface for message services such as SMTP, POP3, or IMAP. + /// + /// + /// Implemented by + /// and . + /// + public interface IMailService : IDisposable + { + /// + /// Gets an object that can be used to synchronize access to the folder. + /// + /// + /// Gets an object that can be used to synchronize access to the folder. + /// + /// The sync root. + object SyncRoot { get; } + + /// + /// Gets or sets the SSL and TLS protocol versions that the client is allowed to use. + /// + /// + /// Gets or sets the SSL and TLS protocol versions that the client is allowed to use. + /// By default, MailKit initializes this value to support only TLS v1.0 and greater and + /// does not support any version of SSL due to those protocols no longer being considered + /// secure. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The SSL and TLS protocol versions that are supported. + SslProtocols SslProtocols { get; set; } + + /// + /// Get or set the client SSL certificates. + /// + /// + /// Some servers may require the client SSL certificates in order + /// to allow the user to connect. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The client SSL certificates. + X509CertificateCollection ClientCertificates { get; set; } + + /// + /// Get or set whether connecting via SSL/TLS should check certificate revocation. + /// + /// + /// Gets or sets whether connecting via SSL/TLS should check certificate revocation. + /// Normally, the value of this property should be set to true (the default) for security + /// reasons, but there are times when it may be necessary to set it to false. + /// For example, most Certificate Authorities are probably pretty good at keeping their CRL and/or + /// OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other + /// network problems between the client and the Certificate Authority. When this happens, it becomes + /// impossible to check the revocation status of one or more of the certificates in the chain + /// resulting in an being thrown in the + /// Connect method. If this becomes a problem, + /// it may become desirable to set to false. + /// + /// true if certificate revocation should be checked; otherwise, false. + bool CheckCertificateRevocation { get; set; } + + /// + /// Get or sets a callback function to validate the server certificate. + /// + /// + /// Gets or sets a callback function to validate the server certificate. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// + /// + /// + /// The server certificate validation callback function. + RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + /// + /// Get or set the local IP end point to use when connecting to a remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to a remote host. + /// + /// The local IP end point or null to use the default end point. + IPEndPoint LocalEndPoint { get; set; } + + /// + /// Get or set the proxy client to use when connecting to a remote host. + /// + /// + /// Gets or sets the proxy client to use when connecting to a remote host via any of the + /// Connect methods. + /// + /// The proxy client. + IProxyClient ProxyClient { get; set; } + + /// + /// Get the authentication mechanisms supported by the message service. + /// + /// + /// The authentication mechanisms are queried durring the + /// Connect method. + /// + /// The supported authentication mechanisms. + HashSet AuthenticationMechanisms { get; } + + /// + /// Get whether or not the client is currently authenticated with the mail server. + /// + /// + /// Gets whether or not the client is currently authenticated with the mail server. + /// To authenticate with the mail server, use one of the + /// Authenticate methods + /// or any of the Async alternatives. + /// + /// true if the client is authenticated; otherwise, false. + bool IsAuthenticated { get; } + + /// + /// Get whether or not the service is currently connected. + /// + /// + /// The state is set to true immediately after + /// one of the Connect + /// methods succeeds and is not set back to false until either the client + /// is disconnected via or until a + /// is thrown while attempting to read or write to + /// the underlying network socket. + /// When an is caught, the connection state of the + /// should be checked before continuing. + /// + /// true if the service connected; otherwise, false. + bool IsConnected { get; } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// true if the connection is secure; otherwise, false. + bool IsSecure { get; } + + /// + /// Get or set the timeout for network streaming operations, in milliseconds. + /// + /// + /// Gets or sets the underlying socket stream's + /// and values. + /// + /// The timeout in milliseconds. + int Timeout { get; set; } + + /// + /// Establish a connection to the specified mail server. + /// + /// + /// Establish a connection to the specified mail server. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server. + /// + /// + /// Asynchronously establishes a connection to the specified mail server. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Establish a connection to the specified mail server. + /// + /// + /// Establish a connection to the specified mail server. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server. + /// + /// + /// Asynchronously establishes a connection to the specified mail server. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Establish a connection to the specified mail server using the provided socket. + /// + /// + /// Establish a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided socket. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Establish a connection to the specified mail server using the provided stream. + /// + /// + /// Establish a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided stream. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// + /// An asynchronous task context. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// Asynchronously authenticates using the supplied credentials. + /// If the server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// + /// An asynchronous task context. + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// + /// + /// + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Disconnect the service. + /// + /// + /// Disconnects from the service. + /// If is true, a "QUIT" command will be issued in order to disconnect cleanly. + /// + /// If set to true, a "QUIT" command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously disconnect the service. + /// + /// + /// Asynchronously disconnects from the service. + /// If is true, a "QUIT" command will be issued in order to disconnect cleanly. + /// + /// An asynchronous task context. + /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Ping the message service to keep the connection alive. + /// + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + void NoOp (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously ping the mail server to keep the connection alive. + /// + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when the client has been successfully connected. + /// + /// + /// The event is raised when the client + /// successfully connects to the mail server. + /// + event EventHandler Connected; + + /// + /// Occurs when the client has been disconnected. + /// + /// + /// The event is raised whenever the client + /// has been disconnected. + /// + event EventHandler Disconnected; + + /// + /// Occurs when the client has been successfully authenticated. + /// + /// + /// The event is raised whenever the client + /// has been authenticated. + /// + event EventHandler Authenticated; + } +} diff --git a/src/MailKit/IMailSpool.cs b/src/MailKit/IMailSpool.cs new file mode 100644 index 0000000..bc78eda --- /dev/null +++ b/src/MailKit/IMailSpool.cs @@ -0,0 +1,511 @@ +// +// IMailSpool.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// An interface for retreiving messages from a spool. + /// + /// + /// An interface for retreiving messages from a spool. + /// + public interface IMailSpool : IMailService, IEnumerable + { + /// + /// Get the number of messages available in the message spool. + /// + /// + /// Gets the number of messages available in the message spool. + /// Once authenticated, the property will be set + /// to the number of available messages in the spool. + /// + /// The message count. + int Count { get; } + + /// + /// Get whether or not the service supports referencing messages by UIDs. + /// + /// + /// Not all servers support referencing messages by UID, so this property should + /// be checked before using + /// and . + /// If the server does not support UIDs, then all methods that take UID arguments + /// along with and + /// will fail. + /// + /// true if supports uids; otherwise, false. + bool SupportsUids { get; } + + /// + /// Get the message count. + /// + /// + /// Gets the message count. + /// + /// The message count. + /// The cancellation token. + int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the UID of the message at the specified index. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the UID of the message at the specified index. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the full list of available message UIDs. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UIDs. + /// The cancellation token. + IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the full list of available message UIDs. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UIDs. + /// The cancellation token. + Task> GetMessageUidsAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the size of the specified message, in bytes. + /// + /// + /// Gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the size of the specified message, in bytes. + /// + /// + /// Asynchronously gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the sizes for all available messages, in bytes. + /// + /// + /// Gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the sizes for all available messages, in bytes. + /// + /// + /// Asynchronously gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + Task> GetMessageSizesAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers for the specified message. + /// + /// + /// Gets the headers for the specified message. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the headers for the specified message. + /// + /// + /// Asynchronously gets the headers for the specified message. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers for the specified messages. + /// + /// + /// Gets the headers for the specified messages. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the headers for the specified messages. + /// + /// + /// Asynchronously gets the headers for the specified messages. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the message at the specified index. + /// + /// + /// Gets the message at the specified index. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message at the specified index. + /// + /// + /// Asynchronously gets the message at the specified index. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the messages at the specified indexes. + /// + /// + /// Gets the messages at the specified indexes. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the messages at the specified indexes. + /// + /// + /// Asynchronously gets the messages at the specified indexes. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the messages within the specified range. + /// + /// + /// Gets the messages within the specified range. + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the messages within the specified range. + /// + /// + /// Asynchronously gets the messages within the specified range. + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header stream at the specified index. + /// + /// + /// Gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header stream at the specified index. + /// + /// + /// Asynchronously gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header streams at the specified index. + /// + /// + /// Gets the message or header streams at the specified index. + /// + /// The message or header streams. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header streams at the specified indexes. + /// + /// + /// Asynchronously gets the message or header streams at the specified indexes. + /// + /// The message or header streams. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header streams within the specified range. + /// + /// + /// Gets the message or header streams within the specified range. + /// + /// The message or header streams. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header streams within the specified range. + /// + /// + /// Asynchronously gets the message or header streams within the specified range. + /// + /// The messages. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The index of the message. + /// The cancellation token. + void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The index of the message. + /// The cancellation token. + Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The indexes of the messages. + /// The cancellation token. + void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The cancellation token. + Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + void Reset (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + Task ResetAsync (CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/IMailStore.cs b/src/MailKit/IMailStore.cs new file mode 100644 index 0000000..dca90aa --- /dev/null +++ b/src/MailKit/IMailStore.cs @@ -0,0 +1,549 @@ +// +// IMailStore.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An interface for retreiving messages from a message store such as IMAP. + /// + /// + /// Implemented by . + /// + public interface IMailStore : IMailService + { + /// + /// Get the personal namespaces. + /// + /// + /// The personal folder namespaces contain a user's personal mailbox folders. + /// + /// The personal namespaces. + FolderNamespaceCollection PersonalNamespaces { get; } + + /// + /// Get the shared namespaces. + /// + /// + /// The shared folder namespaces contain mailbox folders that are shared with the user. + /// + /// The shared namespaces. + FolderNamespaceCollection SharedNamespaces { get; } + + /// + /// Get the other namespaces. + /// + /// + /// The other folder namespaces contain other mailbox folders. + /// + /// The other namespaces. + FolderNamespaceCollection OtherNamespaces { get; } + + /// + /// Get whether or not the mail store supports quotas. + /// + /// + /// Gets whether or not the mail store supports quotas. + /// + /// true if the mail store supports quotas; otherwise, false. + bool SupportsQuotas { get; } + + /// + /// Get the threading algorithms supported by the mail store. + /// + /// + /// The threading algorithms are queried as part of the + /// Connect + /// and Authenticate methods. + /// + /// + /// + /// + /// The threading algorithms. + HashSet ThreadingAlgorithms { get; } + + /// + /// Get the Inbox folder. + /// + /// + /// The Inbox folder is the default folder and is typically the folder + /// where all new messages are delivered. + /// + /// The Inbox folder. + IMailFolder Inbox { get; } + + /// + /// Enable the quick resynchronization feature. + /// + /// + /// Enables quick resynchronization when a folder is opened using the + /// + /// method. + /// If this feature is enabled, the event + /// is replaced with the event. + /// This method needs to be called immediately after + /// , + /// before the opening of any folders. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The mail store does not support quick resynchronization. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously enable the quick resynchronization feature. + /// + /// + /// Enables quick resynchronization when a folder is opened using the + /// + /// method. + /// If this feature is enabled, the event + /// is replaced with the event. + /// This method needs to be called immediately after + /// , + /// before the opening of any folders. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The mail store does not support quick resynchronization. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + Task EnableQuickResyncAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified special folder. + /// + /// + /// Not all message stores support the concept of special folders, + /// so this method may return null. + /// + /// The folder if available; otherwise null. + /// The type of special folder. + /// + /// is out of range. + /// + IMailFolder GetFolder (SpecialFolder folder); + + /// + /// Get the folder for the specified namespace. + /// + /// + /// The main reason to get the toplevel folder in a namespace is + /// to list its child folders. + /// + /// The folder. + /// The namespace. + /// + /// is null. + /// + /// + /// The folder could not be found. + /// + IMailFolder GetFolder (FolderNamespace @namespace); + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Asynchronously gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Asynchronously gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the folder for the specified path. + /// + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the folder for the specified path. + /// + /// + /// Asynchronously gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + Task GetFolderAsync (string path, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sets the specified metadata. + /// + /// + /// Asynchronously sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when a remote message store receives an alert message from the server. + /// + /// + /// Some implementations, such as , + /// will emit Alert events when they receive alert messages from the server. + /// + event EventHandler Alert; + + /// + /// Occurs when a folder is created. + /// + /// + /// The event is emitted when a new folder is created. + /// + event EventHandler FolderCreated; + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + event EventHandler MetadataChanged; + } +} diff --git a/src/MailKit/IMailTransport.cs b/src/MailKit/IMailTransport.cs new file mode 100644 index 0000000..d51a88a --- /dev/null +++ b/src/MailKit/IMailTransport.cs @@ -0,0 +1,179 @@ +// +// IMailTransport.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// An interface for sending messages. + /// + /// + /// An interface for sending messages. + /// + public interface IMailTransport : IMailService + { + /// + /// Send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + void Send (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message. + /// + /// + /// Asynchronously sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// An asynchronous task context. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the specified message using the supplied sender and recipients. + /// + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + void Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Asynchronously sends the specified message using the supplied sender and recipients. + /// + /// An asynchronous task context. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message. + /// + /// + /// Asynchronously sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the specified message using the supplied sender and recipients. + /// + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Asynchronously sends the specified message using the supplied sender and recipients. + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Occurs when a message is successfully sent via the transport. + /// + /// + /// The event will be emitted each time a message is successfully sent. + /// + event EventHandler MessageSent; + } +} diff --git a/src/MailKit/IMessageSummary.cs b/src/MailKit/IMessageSummary.cs new file mode 100644 index 0000000..6a90e93 --- /dev/null +++ b/src/MailKit/IMessageSummary.cs @@ -0,0 +1,460 @@ +// +// IMessageSummary.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// A summary of a message. + /// + /// + /// The Fetch and + /// FetchAsync methods + /// return lists of items. + /// The properties of the that will be available + /// depend on the passed to the aformentioned method. + /// + public interface IMessageSummary + { + /// + /// Get the folder that the message belongs to. + /// + /// + /// Gets the folder that the message belongs to, if available. + /// + /// The folder. + IMailFolder Folder { + get; + } + + /// + /// Get a bitmask of fields that have been populated. + /// + /// + /// Gets a bitmask of fields that have been populated. + /// + /// The fields that have been populated. + MessageSummaryItems Fields { get; } + + /// + /// Gets the body structure of the message, if available. + /// + /// + /// The body will be one of , + /// , , + /// or . + /// This property will only be set if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The body structure of the message. + BodyPart Body { get; } + + /// + /// Gets the text body part of the message if it exists. + /// + /// + /// Gets the text/plain body part of the message. + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// + /// + /// + /// The text body if it exists; otherwise, null. + BodyPartText TextBody { get; } + + /// + /// Gets the html body part of the message if it exists. + /// + /// + /// Gets the text/html body part of the message. + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The html body if it exists; otherwise, null. + BodyPartText HtmlBody { get; } + + /// + /// Gets the body parts of the message. + /// + /// + /// Traverses over the , enumerating all of the + /// objects. + /// This property will only be usable if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The body parts. + IEnumerable BodyParts { get; } + + /// + /// Gets the attachments. + /// + /// + /// Traverses over the , enumerating all of the + /// objects that have a Content-Disposition + /// header set to "attachment". + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// + /// + /// + /// The attachments. + IEnumerable Attachments { get; } + + /// + /// Gets the preview text of the message. + /// + /// + /// The preview text is a short snippet of the beginning of the message + /// text, typically shown in a mail client's message list to provide the user + /// with a sense of what the message is about. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The preview text. + string PreviewText { get; } + + /// + /// Gets the envelope of the message, if available. + /// + /// + /// The envelope of a message contains information such as the + /// date the message was sent, the subject of the message, + /// the sender of the message, who the message was sent to, + /// which message(s) the message may be in reply to, + /// and the message id. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The envelope of the message. + Envelope Envelope { get; } + + /// + /// Gets the normalized subject. + /// + /// + /// A normalized Subject header value where prefixes such as "Re:", "Re[#]:" and "FWD:" have been pruned. + /// This property is typically used for threading messages by subject. + /// + /// The normalized subject. + string NormalizedSubject { get; } + + /// + /// Gets the Date header value. + /// + /// + /// Gets the Date header value. If the Date header is not present, the arrival date is used. + /// If neither are known, is returned. + /// + /// The date. + DateTimeOffset Date { get; } + + /// + /// Gets whether or not the message is a reply. + /// + /// + /// This value should be based on whether the message subject contained any "Re:", "Re[#]:" or "FWD:" prefixes. + /// + /// true if the message is a reply; otherwise, false. + bool IsReply { get; } + + /// + /// Gets the message flags, if available. + /// + /// + /// Gets the message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message flags. + MessageFlags? Flags { get; } + + /// + /// Gets the user-defined message flags, if available. + /// + /// + /// Gets the user-defined message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The user-defined message flags. + HashSet Keywords { get; } + + /// + /// Gets the user-defined message flags, if available. + /// + /// + /// Gets the user-defined message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The user-defined message flags. + [Obsolete ("Use Keywords instead.")] + HashSet UserFlags { get; } + + /// + /// Gets the message annotations, if available. + /// + /// + /// Gets the message annotations, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message annotations. + IList Annotations { get; } + + /// + /// Gets the list of headers, if available. + /// + /// + /// Gets the list of headers, if available. + /// This property will only be set if + /// is specified in a call to one of the + /// Fetch + /// or FetchAsync + /// methods or specific headers are requested via a one of the Fetch or FetchAsync methods + /// that accept list of specific headers to request for each message such as + /// . + /// + /// + /// The list of headers. + HeaderList Headers { get; } + + /// + /// Gets the internal date of the message, if available. + /// + /// + /// Gets the internal date of the message (often the same date as found in the Received header), if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The internal date of the message. + DateTimeOffset? InternalDate { get; } + + /// + /// Gets the size of the message, in bytes, if available. + /// + /// + /// Gets the size of the message, in bytes, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The size of the message. + uint? Size { get; } + + /// + /// Gets the mod-sequence value for the message, if available. + /// + /// + /// Gets the mod-sequence value for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The mod-sequence value. + ulong? ModSeq { get; } + + /// + /// Gets the message-ids that the message references, if available. + /// + /// + /// Gets the message-ids that the message references, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The references. + MessageIdList References { get; } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + string EmailId { get; } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + [Obsolete ("Use EmailId instead.")] + string Id { get; } + + /// + /// Get the globally unique thread identifier for the message, if available. + /// + /// + /// Gets the globally unique thread identifier for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the THREADID value defined in the + /// OBJECTID extension. + /// + /// The globally unique thread identifier. + string ThreadId { get; } + + /// + /// Gets the unique identifier of the message, if available. + /// + /// + /// Gets the unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The uid of the message. + UniqueId UniqueId { get; } + + /// + /// Gets the index of the message. + /// + /// + /// Gets the index of the message. + /// This property is always set. + /// + /// The index of the message. + int Index { get; } + + #region GMail extension properties + + /// + /// Gets the GMail message identifier, if available. + /// + /// + /// Gets the GMail message identifier, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail message identifier. + ulong? GMailMessageId { get; } + + /// + /// Gets the GMail thread identifier, if available. + /// + /// + /// Gets the GMail thread identifier, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail thread identifier. + ulong? GMailThreadId { get; } + + /// + /// Gets the list of GMail labels, if available. + /// + /// + /// Gets the list of GMail labels, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail labels. + IList GMailLabels { get; } + + #endregion + } +} diff --git a/src/MailKit/IProtocolLogger.cs b/src/MailKit/IProtocolLogger.cs new file mode 100644 index 0000000..03c07d7 --- /dev/null +++ b/src/MailKit/IProtocolLogger.cs @@ -0,0 +1,116 @@ +// +// IProtocolLogger.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An interface for logging the communication between a client and server. + /// + /// + /// An interface for logging the communication between a client and server. + /// + /// + /// + /// + public interface IProtocolLogger : IDisposable + { + /// + /// Logs a connection to the specified URI. + /// + /// + /// Logs a connection to the specified URI. + /// + /// The URI. + /// + /// is null. + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + void LogConnect (Uri uri); + + /// + /// Logs a sequence of bytes sent by the client. + /// + /// + /// Logs a sequence of bytes sent by the client. + /// is called by the upon every successful + /// write operation to its underlying network stream, passing the exact same , + /// , and arguments to the logging function. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + void LogClient (byte[] buffer, int offset, int count); + + /// + /// Logs a sequence of bytes sent by the server. + /// + /// + /// Logs a sequence of bytes sent by the server. + /// is called by the upon every successful + /// read of its underlying network stream with the exact buffer that was read. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + void LogServer (byte[] buffer, int offset, int count); + } +} diff --git a/src/MailKit/ITransferProgress.cs b/src/MailKit/ITransferProgress.cs new file mode 100644 index 0000000..4a6ecb9 --- /dev/null +++ b/src/MailKit/ITransferProgress.cs @@ -0,0 +1,58 @@ +// +// ITransferProgress.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// An interface for reporting progress of uploading or downloading messages. + /// + /// + /// An interface for reporting progress of uploading or downloading messages. + /// + public interface ITransferProgress + { + /// + /// Report the progress of the transfer operation. + /// + /// + /// Reports the progress of the transfer operation. + /// This method is only used if the operation knows the size + /// of the message, part, or stream being transferred without doing + /// extra work to calculate it. + /// + /// The number of bytes transferred. + /// The total size, in bytes, of the message, part, or stream being transferred. + void Report (long bytesTransferred, long totalSize); + + /// + /// Report the progress of the transfer operation. + /// + /// + /// Reports the progress of the transfer operation. + /// + /// The number of bytes transferred. + void Report (long bytesTransferred); + } +} diff --git a/src/MailKit/MailFolder.cs b/src/MailKit/MailFolder.cs new file mode 100644 index 0000000..9cd9ef8 --- /dev/null +++ b/src/MailKit/MailFolder.cs @@ -0,0 +1,16501 @@ +// +// MailFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Search; + +namespace MailKit { + /// + /// An abstract mail folder implementation. + /// + /// + /// An abstract mail folder implementation. + /// + public abstract class MailFolder : IMailFolder + { + /// + /// The bit mask of settable flags. + /// + /// + /// Only flags in the list of settable flags may be set on a message by the client. + /// + protected static readonly MessageFlags SettableFlags = MessageFlags.Answered | MessageFlags.Deleted | + MessageFlags.Draft | MessageFlags.Flagged | MessageFlags.Seen; + + IMailFolder parent; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + protected MailFolder () + { + } + + /// + /// Get an object that can be used to synchronize access to the folder. + /// + /// + /// Gets an object that can be used to synchronize access to the folder. + /// + /// The sync root. + public abstract object SyncRoot { + get; + } + + /// + /// Get the parent folder. + /// + /// + /// Root-level folders do not have a parent folder. + /// + /// The parent folder. + public IMailFolder ParentFolder { + get { return parent; } + internal protected set { + if (value == parent) + return; + + if (parent != null) + parent.Renamed -= OnParentFolderRenamed; + + parent = value; + + if (parent != null) + parent.Renamed += OnParentFolderRenamed; + } + } + + /// + /// Get the folder attributes. + /// + /// + /// Gets the folder attributes. + /// + /// The folder attributes. + public FolderAttributes Attributes { + get; internal protected set; + } + + /// + /// Get the annotation access level. + /// + /// + /// If annotations are supported, this property can be used to determine whether or not + /// the supports reading and writing annotations. + /// + public AnnotationAccess AnnotationAccess { + get; internal protected set; + } + + /// + /// Get the supported annotation scopes. + /// + /// + /// If annotations are supported, this property can be used to determine which + /// annotation scopes are supported by the . + /// + public AnnotationScope AnnotationScopes { + get; internal protected set; + } + + /// + /// Get the maximum size of annotation values supported by the folder. + /// + /// + /// If annotations are supported, this property can be used to determine the + /// maximum size of annotation values supported by the . + /// + public uint MaxAnnotationSize { + get; internal protected set; + } + + /// + /// Get the permanent flags. + /// + /// + /// The permanent flags are the message flags that will persist between sessions. + /// If the flag is set, then the folder allows + /// storing of user-defined (custom) message flags. + /// + /// The permanent flags. + public MessageFlags PermanentFlags { + get; protected set; + } + + /// + /// Get the accepted flags. + /// + /// + /// The accepted flags are the message flags that will be accepted and persist + /// for the current session. For the set of flags that will persist between + /// sessions, see the property. + /// + /// The accepted flags. + public MessageFlags AcceptedFlags { + get; protected set; + } + + /// + /// Get the directory separator. + /// + /// + /// Gets the directory separator. + /// + /// The directory separator. + public char DirectorySeparator { + get; protected set; + } + + /// + /// Get the read/write access of the folder. + /// + /// + /// Gets the read/write access of the folder. + /// + /// The read/write access. + public FolderAccess Access { + get; internal protected set; + } + + /// + /// Get whether or not the folder is a namespace folder. + /// + /// + /// Gets whether or not the folder is a namespace folder. + /// + /// true if the folder is a namespace folder; otherwise, false. + public bool IsNamespace { + get; protected set; + } + + /// + /// Get the full name of the folder. + /// + /// + /// This is the equivalent of the full path of a file on a file system. + /// + /// The full name of the folder. + public string FullName { + get; protected set; + } + + /// + /// Get the name of the folder. + /// + /// + /// This is the equivalent of the file name of a file on the file system. + /// + /// The name of the folder. + public string Name { + get; protected set; + } + + /// + /// Get the unique identifier for the folder, if available. + /// + /// + /// Gets a unique identifier for the folder, if available. This is useful for clients + /// implementing a message cache that want to track the folder after it is renamed by another + /// client. + /// This property will only be available if the server supports the + /// OBJECTID extension. + /// + /// The unique folder identifier. + public string Id { + get; protected set; + } + + /// + /// Get a value indicating whether the folder is subscribed. + /// + /// + /// Gets a value indicating whether the folder is subscribed. + /// + /// true if the folder is subscribed; otherwise, false. + public bool IsSubscribed { + get { return (Attributes & FolderAttributes.Subscribed) != 0; } + } + + /// + /// Get a value indicating whether the folder is currently open. + /// + /// + /// Gets a value indicating whether the folder is currently open. + /// + /// true if the folder is currently open; otherwise, false. + public abstract bool IsOpen { + get; + } + + /// + /// Get a value indicating whether the folder exists. + /// + /// + /// Gets a value indicating whether the folder exists. + /// + /// true if the folder exists; otherwise, false. + public bool Exists { + get { return (Attributes & FolderAttributes.NonExistent) == 0; } + } + + /// + /// Get whether or not the folder supports mod-sequences. + /// + /// + /// Gets whether or not the folder supports mod-sequences. + /// If mod-sequences are not supported by the folder, then all of the APIs that take a modseq + /// argument will throw and should not be used. + /// + /// true if supports mod-sequences; otherwise, false. + [Obsolete ("Use Supports (FolderFeature.ModSequences) instead.")] + public bool SupportsModSeq { + get { return Supports (FolderFeature.ModSequences); } + } + + /// + /// Get the highest mod-sequence value of all messages in the mailbox. + /// + /// + /// Gets the highest mod-sequence value of all messages in the mailbox. + /// + /// The highest mod-sequence value. + public ulong HighestModSeq { + get; protected set; + } + + /// + /// Get the UID validity. + /// + /// + /// UIDs are only valid so long as the UID validity value remains unchanged. If and when + /// the folder's is changed, a client MUST discard its cache of UIDs + /// along with any summary information that it may have and re-query the folder. + /// This value will only be set after the folder has been opened. + /// + /// The UID validity. + public uint UidValidity { + get; protected set; + } + + /// + /// Get the UID that the folder will assign to the next message that is added. + /// + /// + /// This value will only be set after the folder has been opened. + /// + /// The next UID. + public UniqueId? UidNext { + get; protected set; + } + + /// + /// Get the maximum size of a message that can be appended to the folder. + /// + /// + /// Gets the maximum size of a message that can be appended to the folder. + /// If the value is not set, then the limit is unspecified. + /// + /// The append limit. + public uint? AppendLimit { + get; protected set; + } + + /// + /// Get the size of the folder. + /// + /// + /// Gets the size of the folder in bytes. + /// If the value is not set, then the size is unspecified. + /// + /// The size. + public ulong? Size { + get; protected set; + } + + /// + /// Get the index of the first unread message in the folder. + /// + /// + /// This value will only be set after the folder has been opened. + /// + /// The index of the first unread message. + public int FirstUnread { + get; protected set; + } + + /// + /// Get the number of unread messages in the folder. + /// + /// + /// Gets the number of unread messages in the folder. + /// This value will only be set after calling + /// + /// with . + /// + /// The number of unread messages. + public int Unread { + get; protected set; + } + + /// + /// Get the number of recently added messages in the folder. + /// + /// + /// Gets the number of recently delivered messages in the folder. + /// This value will only be set after calling + /// + /// with or by opening the folder. + /// + /// The number of recently added messages. + public int Recent { + get; protected set; + } + + /// + /// Get the total number of messages in the folder. + /// + /// + /// Gets the total number of messages in the folder. + /// This value will only be set after calling + /// + /// with or by opening the folder. + /// + /// The total number of messages. + public int Count { + get; protected set; + } + + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Get the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + public abstract HashSet ThreadingAlgorithms { get; } + + /// + /// Determine whether or not a supports a feature. + /// + /// + /// Determines whether or not a supports a feature. + /// + /// The desired feature. + /// true if the feature is supported; otherwise, false. + public abstract bool Supports (FolderFeature feature); + + /// + /// Opens the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The quick resynchronization feature has not been enabled. + /// + /// + /// The mail store does not support the quick resynchronization feature. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously opens the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The quick resynchronization feature has not been enabled. + /// + /// + /// The mail store does not support the quick resynchronization feature. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Open the folder using the requested folder access. + /// + /// + /// Opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously open the folder using the requested folder access. + /// + /// + /// Asynchronously opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Closes the folder, optionally expunging the messages marked for deletion. + /// + /// If set to true, expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Asynchronously closes the folder, optionally expunging the messages marked for deletion. + /// + /// An asynchronous task context. + /// If set to true, expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// The special use for the folder being created. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IMailFolder Create (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)) + { + return Create (name, new [] { specialUse }, cancellationToken); + } + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Asynchronously creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// The special use for the folder being created. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The does not support the creation of special folders. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task CreateAsync (string name, SpecialFolder specialUse, CancellationToken cancellationToken = default (CancellationToken)) + { + return CreateAsync (name, new [] { specialUse }, cancellationToken); + } + + /// + /// Rename the folder. + /// + /// + /// Renames the folder. + /// + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously rename the folder. + /// + /// + /// Asynchronously renames the folder. + /// + /// An asynchronous task context. + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Delete the folder. + /// + /// + /// Deletes the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Delete (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously delete the folder. + /// + /// + /// Asynchronously deletes the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task DeleteAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Subscribe to the folder. + /// + /// + /// Subscribes to the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Subscribe (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously subscribe to the folder. + /// + /// + /// Asynchronously subscribes to the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Unsubscribe from the folder. + /// + /// + /// Unsubscribes from the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously unsubscribe from the folder. + /// + /// + /// Asynchronously unsubscribes from the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task UnsubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the subfolders. + /// + /// + /// Gets the subfolders as well as queries the server for the status of the requested items. + /// When the argument is non-empty, this has the equivalent functionality + /// of calling and then calling + /// on each of the returned folders. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the subfolders. + /// + /// + /// Asynchronously gets the subfolders as well as queries the server for the status of the requested items. + /// When the argument is non-empty, this has the equivalent functionality + /// of calling and then calling + /// on each of the returned folders. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the subfolders. + /// + /// + /// Gets the subfolders. + /// + /// The subfolders. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList GetSubfolders (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfolders (StatusItems.None, subscribedOnly, cancellationToken); + } + + /// + /// Asynchronously get the subfolders. + /// + /// + /// Asynchronously gets the subfolders. + /// + /// The subfolders. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> GetSubfoldersAsync (bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfoldersAsync (StatusItems.None, subscribedOnly, cancellationToken); + } + + /// + /// Get the specified subfolder. + /// + /// + /// Gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the specified subfolder. + /// + /// + /// Asynchronously gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Force the server to flush its state for the folder. + /// + /// + /// Forces the server to flush its state for the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Check (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously force the server to flush its state for the folder. + /// + /// + /// Asynchronously forces the server to flush its state for the folder. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CheckAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// + /// The items to update. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The mail store does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// + /// An asynchronous task context. + /// The items to update. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the complete access control list for the folder. + /// + /// + /// Gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the complete access control list for the folder. + /// + /// + /// Asynchronously gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetAccessControlListAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the access rights for a particular identifier. + /// + /// + /// Gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the access rights for a particular identifier. + /// + /// + /// Asynchronously gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the access rights for the current authenticated user. + /// + /// + /// Gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the access rights for the current authenticated user. + /// + /// + /// Asynchronously gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add access rights for the specified identity. + /// + /// + /// Adds the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add access rights for the specified identity. + /// + /// + /// Asynchronously adds the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove access rights for the specified identity. + /// + /// + /// Removes the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove access rights for the specified identity. + /// + /// + /// Asynchronously removes the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the access rights for the specified identity. + /// + /// + /// Sets the access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the access rights for the specified identity. + /// + /// + /// Asynchronously sets the access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove all access rights for the given identity. + /// + /// + /// Removes all access rights for the given identity. + /// + /// The identity name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove all access rights for the given identity. + /// + /// + /// Asynchronously removes all access rights for the given identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the quota information for the folder. + /// + /// + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the quota information for the folder. + /// + /// + /// Asynchronously gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetQuotaAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the quota limits for the folder. + /// + /// + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The updated folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the quota limits for the folder. + /// + /// + /// Asynchronously sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The updated folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail store does not support quotas. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadata (new MetadataOptions (), tags, cancellationToken); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (new MetadataOptions (), tags, cancellationToken); + } + + /// + /// Get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sets the specified metadata. + /// + /// + /// Asynchronously sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// Expunges the folder, permanently removing all messages marked for deletion. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Expunge (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// Asynchronously expunges the folder, permanently removing all messages marked for deletion. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task ExpungeAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Expunges the specified uids, permanently removing them from the folder. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// The message uids. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Asynchronously expunges the specified uids, permanently removing them from the folder. + /// Normally, an event will be emitted for + /// each message that is expunged. However, if the mail store supports the quick + /// resynchronization feature and it has been enabled via the + /// method, then + /// the event will be emitted rather than the + /// event. + /// + /// An asynchronous task context. + /// The message uids. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual UniqueId? Append (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Append (FormatOptions.Default, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AppendAsync (MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (FormatOptions.Default, message, flags, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Append (FormatOptions.Default, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (FormatOptions.Default, message, flags, date, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// One or more does not define any properties. + /// " + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual UniqueId? Append (MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Append (FormatOptions.Default, message, flags, date, annotations, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// One or more does not define any properties. + /// " + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AppendAsync (MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (FormatOptions.Default, message, flags, date, annotations, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Asynchronously appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList Append (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Append (FormatOptions.Default, messages, flags, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> AppendAsync (IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (FormatOptions.Default, messages, flags, cancellationToken, progress); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList Append (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Append (FormatOptions.Default, messages, flags, dates, cancellationToken, progress); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> AppendAsync (IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (FormatOptions.Default, messages, flags, dates, cancellationToken, progress); + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AppendAsync (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Asynchronously appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The array of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual UniqueId? Replace (UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Replace (FormatOptions.Default, uid, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task ReplaceAsync (UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (FormatOptions.Default, uid, message, flags, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual UniqueId? Replace (UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Replace (FormatOptions.Default, uid, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task ReplaceAsync (UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (FormatOptions.Default, uid, message, flags, date, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual UniqueId? Replace (int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Replace (FormatOptions.Default, index, message, flags, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task ReplaceAsync (int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (FormatOptions.Default, index, message, flags, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual UniqueId? Replace (int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return Replace (FormatOptions.Default, index, message, flags, date, cancellationToken, progress); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task ReplaceAsync (int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (FormatOptions.Default, index, message, flags, date, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public abstract Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Copy the specified message to the destination folder. + /// + /// + /// Copies the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual UniqueId? CopyTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + var uids = CopyTo (new [] { uid }, destination, cancellationToken); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; + } + + /// + /// Asynchronously copy the specified message to the destination folder. + /// + /// + /// Asynchronously copies the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual async Task CopyToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + var uids = await CopyToAsync (new [] { uid }, destination, cancellationToken).ConfigureAwait (false); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; + } + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Asynchronously copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified message to the destination folder. + /// + /// + /// Moves the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual UniqueId? MoveTo (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + var uids = MoveTo (new [] { uid }, destination, cancellationToken); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; + } + + /// + /// Asynchronously move the specified message to the destination folder. + /// + /// + /// Asynchronously moves the specified message to the destination folder. + /// + /// The UID of the message in the destination folder, if available; otherwise, null. + /// The UID of the message to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual async Task MoveToAsync (UniqueId uid, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + var uids = await MoveToAsync (new [] { uid }, destination, cancellationToken).ConfigureAwait (false); + + if (uids != null && uids.Destination.Count > 0) + return uids.Destination[0]; + + return null; + } + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// Asynchronously moves the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The mail store does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Copy the specified message to the destination folder. + /// + /// + /// Copies the specified message to the destination folder. + /// + /// The index of the message to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void CopyTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + CopyTo (new [] { index }, destination, cancellationToken); + } + + /// + /// Asynchronously copy the specified message to the destination folder. + /// + /// + /// Asynchronously copies the specified message to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the message to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task CopyToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + return CopyToAsync (new [] { index }, destination, cancellationToken); + } + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Asynchronously copies the specified messages to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Move the specified message to the destination folder. + /// + /// + /// Moves the specified message to the destination folder. + /// + /// The index of the message to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void MoveTo (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + MoveTo (new [] { index }, destination, cancellationToken); + } + + /// + /// Asynchronously move the specified message to the destination folder. + /// + /// + /// Asynchronously moves the specified message to the destination folder. + /// + /// An asynchronous task context. + /// The index of the message to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// does not refer to a valid message index. + /// + /// + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task MoveToAsync (int index, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + return MoveToAsync (new [] { index }, destination, cancellationToken); + } + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// Asynchronously moves the specified messages to the destination folder. + /// + /// An asynchronous task context. + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the mail store supports quick resynchronization and the application has + /// enabled this feature via , + /// then this method will emit events for messages that + /// have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the specified message + /// indexes that have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Fetch the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously fetch the message summaries for the messages between the two indexes + /// (inclusive) that have a higher mod-sequence value than the one specified. + /// + /// + /// Asynchronously fetches the message summaries for the messages between + /// the two indexes (inclusive) that have a higher mod-sequence value than the + /// one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the mail service may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Asynchronously gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Asynchronously gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Asynchronously gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Asynchronously gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message. + /// + /// + /// Asynchronously gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified message. + /// + /// + /// Asynchronously gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Asynchronously gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Asynchronously gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Stream GetStream (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (uid.Id == 0) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStream (uid, part.PartSpecifier, offset, count, cancellationToken, progress); + } + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task GetStreamAsync (UniqueId uid, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStreamAsync (uid, part.PartSpecifier, offset, count, cancellationToken, progress); + } + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Stream GetStream (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStream (index, part.PartSpecifier, offset, count, cancellationToken, progress); + } + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the body part. If the starting offset is beyond + /// the end of the body part, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the body part, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The desired body part. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task GetStreamAsync (int index, BodyPart part, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + return GetStreamAsync (index, part.PartSpecifier, offset, count, cancellationToken, progress); + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified message. + /// + /// + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get a substream of the specified body part. + /// + /// + /// Asynchronously gets a substream of the specified message. If the starting + /// offset is beyond the end of the specified section of the message, an empty stream + /// is returned. If the number of bytes desired extends beyond the end of the section, + /// a truncated stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The UID of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The UID of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (uids, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (uids, flags, null, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void AddFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AddFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The UIDs of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The UIDs of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (uids, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (uids, flags, null, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The UIDs of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (UniqueId uid, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (new [] { uid }, flags, silent, cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The UIDs of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (UniqueId uid, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (new [] { uid }, flags, keywords, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (uids, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (IList uids, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (uids, flags, null, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList AddFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlags (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlags (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList SetFlags (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlags (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (uids, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The index of the message. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The index of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified message. + /// + /// + /// Adds a set of flags to the specified message. + /// + /// The index of the message. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified message. + /// + /// + /// Asynchronously adds a set of flags to the specified message. + /// + /// An asynchronous task context. + /// The index of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The indexes of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddFlags (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void AddFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages. + /// + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AddFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The index of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified message. + /// + /// + /// Removes a set of flags from the specified message. + /// + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified message. + /// + /// + /// Asynchronously removes a set of flags from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The indexes of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveFlags (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages. + /// + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The index of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (int index, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (new [] { index }, flags, silent, cancellationToken); + } + + /// + /// Set the flags of the specified message. + /// + /// + /// Sets the flags of the specified message. + /// + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified message. + /// + /// + /// Asynchronously sets the flags of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (int index, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (new [] { index }, flags, keywords, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The indexes of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetFlags (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetFlags (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetFlagsAsync (IList indexes, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (indexes, flags, null, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages. + /// + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlags (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddFlagsAsync (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlags (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveFlagsAsync (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlags (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetFlagsAsync (indexes, modseq, flags, null, silent, cancellationToken); + } + + /// + /// Set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The UID of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddLabels (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddLabelsAsync (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AddLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The UIDs of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveLabels (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveLabelsAsync (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The UIDs of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetLabels (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetLabels (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetLabelsAsync (UniqueId uid, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetLabelsAsync (new [] { uid }, labels, silent, cancellationToken); + } + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AddLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> RemoveLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> SetLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified message. + /// + /// + /// Adds a set of labels to the specified message. + /// + /// The index of the message. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void AddLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + AddLabels (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously add a set of labels to the specified message. + /// + /// + /// Asynchronously adds a set of labels to the specified message. + /// + /// An asynchronous task context. + /// The index of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task AddLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return AddLabelsAsync (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Asynchronously adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task AddLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified message. + /// + /// + /// Removes a set of labels from the specified message. + /// + /// The index of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void RemoveLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveLabels (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously remove a set of labels from the specified message. + /// + /// + /// Asynchronously removes a set of labels from the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task RemoveLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveLabelsAsync (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Asynchronously removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task RemoveLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified message. + /// + /// + /// Sets the labels of the specified message. + /// + /// The index of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void SetLabels (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + SetLabels (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Asynchronously set the labels of the specified message. + /// + /// + /// Asynchronously sets the labels of the specified message. + /// + /// An asynchronous task context. + /// The index of the message. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SetLabelsAsync (int index, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetLabelsAsync (new [] { index }, labels, silent, cancellationToken); + } + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> AddLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> RemoveLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> SetLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void Store (UniqueId uid, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + Store (new [] { uid }, annotations, cancellationToken); + } + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The UID of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task StoreAsync (UniqueId uid, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (new [] { uid }, annotations, cancellationToken); + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Store (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified message. + /// + /// + /// Stores the annotations for the specified message. + /// + /// The index of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual void Store (int index, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + Store (new[] { index }, annotations, cancellationToken); + } + + /// + /// Asynchronously store the annotations for the specified message. + /// + /// + /// Asynchronously stores the annotations for the specified message. + /// + /// An asynchronous task context. + /// The indexes of the message. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task StoreAsync (int index, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (new [] { index }, annotations, cancellationToken); + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Task.Factory.StartNew (() => { + lock (SyncRoot) { + return Search (query, cancellationToken); + } + }, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + } + + /// + /// Search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the mail store. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList Search (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Search (uidSet.And (query), cancellationToken); + } + + /// + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> SearchAsync (IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SearchAsync (uidSet.And (query), cancellationToken); + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// Searches the fsubset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual SearchResults Search (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Search (options, uidSet.And (query), cancellationToken); + } + + /// + /// Asynchronously search the subset of UIDs in the folder for messages matching the specified query. + /// + /// + /// Asynchronously searches the fsubset of UIDs in the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SearchAsync (SearchOptions options, IList uids, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SearchAsync (options, uidSet.And (query), cancellationToken); + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual IList Sort (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Sort (uidSet.And (query), orderBy, cancellationToken); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task> SortAsync (IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SortAsync (uidSet.And (query), orderBy, cancellationToken); + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual SearchResults Sort (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return Sort (options, uidSet.And (query), orderBy, cancellationToken); + } + + /// + /// Asynchronously sort messages matching the specified query, returning the search results in the specified sort order. + /// + /// + /// Asynchronously searches the folder for messages matching the specified query, + /// returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The subset of UIDs + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// is empty. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support the specified search options. + /// -or- + /// The server does not support sorting search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task SortAsync (SearchOptions options, IList uids, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + var uidSet = new UidSearchQuery (uids); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + return SortAsync (options, uidSet.And (query), orderBy, cancellationToken); + } + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported. + /// -or- + /// The server does not support threading search results. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when the folder is opened. + /// + /// + /// The event is emitted when the folder is opened. + /// + public event EventHandler Opened; + + /// + /// Raise the opened event. + /// + /// + /// Raises the opened event. + /// + protected virtual void OnOpened () + { + Opened?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder is closed. + /// + /// + /// The event is emitted when the folder is closed. + /// + public event EventHandler Closed; + + /// + /// Raise the closed event. + /// + /// + /// Raises the closed event. + /// + internal protected virtual void OnClosed () + { + PermanentFlags = MessageFlags.None; + AcceptedFlags = MessageFlags.None; + Access = FolderAccess.None; + + AnnotationAccess = AnnotationAccess.None; + AnnotationScopes = AnnotationScope.None; + MaxAnnotationSize = 0; + + Closed?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder is deleted. + /// + /// + /// The event is emitted when the folder is deleted. + /// + public event EventHandler Deleted; + + /// + /// Raise the deleted event. + /// + /// + /// Raises the deleted event. + /// + protected virtual void OnDeleted () + { + Deleted?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder is renamed. + /// + /// + /// The event is emitted when the folder is renamed. + /// + public event EventHandler Renamed; + + /// + /// Raise the renamed event. + /// + /// + /// Raises the renamed event. + /// + /// The old name of the folder. + /// The new name of the folder. + protected virtual void OnRenamed (string oldName, string newName) + { + Renamed?.Invoke (this, new FolderRenamedEventArgs (oldName, newName)); + } + + /// + /// Notifies the folder that a parent folder has been renamed. + /// + /// + /// implementations should override this method + /// to update their state (such as updating their + /// property). + /// + protected virtual void OnParentFolderRenamed () + { + } + + void OnParentFolderRenamed (object sender, FolderRenamedEventArgs e) + { + var oldFullName = FullName; + + OnParentFolderRenamed (); + + if (FullName != oldFullName) + OnRenamed (oldFullName, FullName); + } + + /// + /// Occurs when the folder is subscribed. + /// + /// + /// The event is emitted when the folder is subscribed. + /// + public event EventHandler Subscribed; + + /// + /// Raise the subscribed event. + /// + /// + /// Raises the subscribed event. + /// + protected virtual void OnSubscribed () + { + Subscribed?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder is unsubscribed. + /// + /// + /// The event is emitted when the folder is unsubscribed. + /// + public event EventHandler Unsubscribed; + + /// + /// Raise the unsubscribed event. + /// + /// + /// Raises the unsubscribed event. + /// + protected virtual void OnUnsubscribed () + { + Unsubscribed?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when a message is expunged from the folder. + /// + /// + /// The event is emitted when a message is expunged from the folder. + /// + /// + /// + /// + public event EventHandler MessageExpunged; + + /// + /// Raise the message expunged event. + /// + /// + /// Raises the message expunged event. + /// + /// The message expunged event args. + protected virtual void OnMessageExpunged (MessageEventArgs args) + { + MessageExpunged?.Invoke (this, args); + } + + /// + /// Occurs when a message vanishes from the folder. + /// + /// + /// The event is emitted when messages vanish from the folder. + /// + public event EventHandler MessagesVanished; + + /// + /// Raise the messages vanished event. + /// + /// + /// Raises the messages vanished event. + /// + /// The messages vanished event args. + protected virtual void OnMessagesVanished (MessagesVanishedEventArgs args) + { + MessagesVanished?.Invoke (this, args); + } + + /// + /// Occurs when flags changed on a message. + /// + /// + /// The event is emitted when the flags for a message are changed. + /// + /// + /// + /// + public event EventHandler MessageFlagsChanged; + + /// + /// Raise the message flags changed event. + /// + /// + /// Raises the message flags changed event. + /// + /// The message flags changed event args. + protected virtual void OnMessageFlagsChanged (MessageFlagsChangedEventArgs args) + { + MessageFlagsChanged?.Invoke (this, args); + } + + /// + /// Occurs when labels changed on a message. + /// + /// + /// The event is emitted when the labels for a message are changed. + /// + public event EventHandler MessageLabelsChanged; + + /// + /// Raise the message labels changed event. + /// + /// + /// Raises the message labels changed event. + /// + /// The message labels changed event args. + protected virtual void OnMessageLabelsChanged (MessageLabelsChangedEventArgs args) + { + MessageLabelsChanged?.Invoke (this, args); + } + + /// + /// Occurs when annotations changed on a message. + /// + /// + /// The event is emitted when the annotations for a message are changed. + /// + public event EventHandler AnnotationsChanged; + + /// + /// Raise the message annotations changed event. + /// + /// + /// Raises the message annotations changed event. + /// + /// The message annotations changed event args. + protected virtual void OnAnnotationsChanged (AnnotationsChangedEventArgs args) + { + AnnotationsChanged?.Invoke (this, args); + } + + /// + /// Occurs when a message summary is fetched from the folder. + /// + /// + /// Emitted when a message summary is fetched from the folder. + /// When multiple message summaries are being fetched from a remote folder, + /// it is possible that the connection will drop or some other exception will + /// occur, causing the Fetch method to fail and lose all of the data that has been + /// downloaded up to that point, requiring the client to request the same set of + /// message summaries all over again after it reconnects. This is obviously + /// inefficient. To alleviate this potential problem, this event will be emitted + /// as soon as the successfully parses each untagged FETCH + /// response from the server, allowing the client to commit this data immediately to + /// its local cache. + /// Depending on the IMAP server, it is possible that the + /// event will be emitted for the same message + /// multiple times if the IMAP server happens to split the requested fields into + /// multiple untagged FETCH responses. Use the + /// property to determine which f properties have + /// been populated. + /// + public event EventHandler MessageSummaryFetched; + + /// + /// Raise the message summary fetched event. + /// + /// + /// Raises the message summary fetched event. + /// When multiple message summaries are being fetched from a remote folder, + /// it is possible that the connection will drop or some other exception will + /// occur, causing the Fetch method to fail and lose all of the data that has been + /// downloaded up to that point, requiring the client to request the same set of + /// message summaries all over again after it reconnects. This is obviously + /// inefficient. To alleviate this potential problem, this event will be emitted + /// as soon as the successfully parses each untagged FETCH + /// response from the server, allowing the client to commit this data immediately to + /// its local cache. + /// Depending on the IMAP server, it is possible that + /// will be invoked for the same message + /// multiple times if the IMAP server happens to split the requested fields into + /// multiple untagged FETCH responses. Use the + /// property to determine which f properties have + /// been populated. + /// + /// The message summary. + protected virtual void OnMessageSummaryFetched (IMessageSummary message) + { + MessageSummaryFetched?.Invoke (this, new MessageSummaryFetchedEventArgs (message)); + } + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + public event EventHandler MetadataChanged; + + /// + /// Raise the metadata changed event. + /// + /// + /// Raises the metadata changed event. + /// + /// The metadata that changed. + internal protected virtual void OnMetadataChanged (Metadata metadata) + { + MetadataChanged?.Invoke (this, new MetadataChangedEventArgs (metadata)); + } + + /// + /// Occurs when the mod-sequence changed on a message. + /// + /// + /// The event is emitted when the mod-sequence for a message is changed. + /// + public event EventHandler ModSeqChanged; + + /// + /// Raise the message mod-sequence changed event. + /// + /// + /// Raises the message mod-sequence changed event. + /// + /// The mod-sequence changed event args. + protected virtual void OnModSeqChanged (ModSeqChangedEventArgs args) + { + ModSeqChanged?.Invoke (this, args); + } + + /// + /// Occurs when the highest mod-sequence changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler HighestModSeqChanged; + + /// + /// Raise the highest mod-sequence changed event. + /// + /// + /// Raises the highest mod-sequence changed event. + /// + protected virtual void OnHighestModSeqChanged () + { + HighestModSeqChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the next UID changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler UidNextChanged; + + /// + /// Raise the next UID changed event. + /// + /// + /// Raises the next UID changed event. + /// + protected virtual void OnUidNextChanged () + { + UidNextChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the UID validity changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler UidValidityChanged; + + /// + /// Raise the uid validity changed event. + /// + /// + /// Raises the uid validity changed event. + /// + protected virtual void OnUidValidityChanged () + { + UidValidityChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder ID changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler IdChanged; + + /// + /// Raise the ID changed event. + /// + /// + /// Raises the ID changed event. + /// + protected virtual void OnIdChanged () + { + IdChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the folder size changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler SizeChanged; + + /// + /// Raise the size changed event. + /// + /// + /// Raises the size changed event. + /// + protected virtual void OnSizeChanged () + { + SizeChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the message count changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + /// + /// + /// + public event EventHandler CountChanged; + + /// + /// Raise the message count changed event. + /// + /// + /// Raises the message count changed event. + /// + protected virtual void OnCountChanged () + { + CountChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the recent message count changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler RecentChanged; + + /// + /// Raise the recent message count changed event. + /// + /// + /// Raises the recent message count changed event. + /// + protected virtual void OnRecentChanged () + { + RecentChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Occurs when the unread message count changes. + /// + /// + /// The event is emitted whenever the value changes. + /// + public event EventHandler UnreadChanged; + + /// + /// Raise the unread message count changed event. + /// + /// + /// Raises the unread message count changed event. + /// + protected virtual void OnUnreadChanged () + { + UnreadChanged?.Invoke (this, EventArgs.Empty); + } + + #region IEnumerable implementation + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + public abstract IEnumerator GetEnumerator (); + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder is not currently open. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return FullName; + } + } +} diff --git a/src/MailKit/MailService.cs b/src/MailKit/MailService.cs new file mode 100644 index 0000000..0dd1673 --- /dev/null +++ b/src/MailKit/MailService.cs @@ -0,0 +1,1582 @@ +// +// MailService.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using SslProtocols = System.Security.Authentication.SslProtocols; + +using MailKit.Net; +using MailKit.Net.Proxy; +using MailKit.Security; + +namespace MailKit { + /// + /// An abstract mail service implementation. + /// + /// + /// An abstract mail service implementation. + /// + public abstract class MailService : IMailService + { +#if NET48 + const SslProtocols DefaultSslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13; +#else + const SslProtocols DefaultSslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12; +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The protocol logger. + /// + /// is null. + /// + protected MailService (IProtocolLogger protocolLogger) + { + if (protocolLogger == null) + throw new ArgumentNullException (nameof (protocolLogger)); + + SslProtocols = DefaultSslProtocols; + CheckCertificateRevocation = true; + ProtocolLogger = protocolLogger; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + protected MailService () : this (new NullProtocolLogger ()) + { + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~MailService () + { + Dispose (false); + } + + /// + /// Gets an object that can be used to synchronize access to the service. + /// + /// + /// Gets an object that can be used to synchronize access to the service. + /// + /// The sync root. + public abstract object SyncRoot { + get; + } + + /// + /// Gets the protocol supported by the message service. + /// + /// + /// Gets the protocol supported by the message service. + /// + /// The protocol. + protected abstract string Protocol { + get; + } + + /// + /// Get the protocol logger. + /// + /// + /// Gets the protocol logger. + /// + /// The protocol logger. + public IProtocolLogger ProtocolLogger { + get; private set; + } + + /// + /// Gets or sets the SSL and TLS protocol versions that the client is allowed to use. + /// + /// + /// Gets or sets the SSL and TLS protocol versions that the client is allowed to use. + /// By default, MailKit initializes this value to support only TLS v1.1 and greater and + /// does not support TLS v1.0 or any version of SSL due to those protocols no longer being considered + /// secure. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The SSL and TLS protocol versions that are supported. + public SslProtocols SslProtocols { + get; set; + } + + /// + /// Gets or sets the client SSL certificates. + /// + /// + /// Some servers may require the client SSL certificates in order + /// to allow the user to connect. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// The client SSL certificates. + public X509CertificateCollection ClientCertificates { + get; set; + } + + /// + /// Get or set whether connecting via SSL/TLS should check certificate revocation. + /// + /// + /// Gets or sets whether connecting via SSL/TLS should check certificate revocation. + /// Normally, the value of this property should be set to true (the default) for security + /// reasons, but there are times when it may be necessary to set it to false. + /// For example, most Certificate Authorities are probably pretty good at keeping their CRL and/or + /// OCSP servers up 24/7, but occasionally they do go down or are otherwise unreachable due to other + /// network problems between the client and the Certificate Authority. When this happens, it becomes + /// impossible to check the revocation status of one or more of the certificates in the chain + /// resulting in an being thrown in the + /// Connect method. If this becomes a problem, + /// it may become desirable to set to false. + /// + /// true if certificate revocation should be checked; otherwise, false. + public bool CheckCertificateRevocation { + get; set; + } + + /// + /// Get or sets a callback function to validate the server certificate. + /// + /// + /// Gets or sets a callback function to validate the server certificate. + /// This property should be set before calling any of the + /// Connect methods. + /// + /// + /// + /// + /// The server certificate validation callback function. + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { + get; set; + } + + /// + /// Get or set the local IP end point to use when connecting to the remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to the remote host. + /// + /// The local IP end point or null to use the default end point. + public IPEndPoint LocalEndPoint { + get; set; + } + + /// + /// Get or set the proxy client to use when connecting to a remote host. + /// + /// + /// Gets or sets the proxy client to use when connecting to a remote host via any of the + /// Connect methods. + /// + /// The proxy client. + public IProxyClient ProxyClient { + get; set; + } + + /// + /// Gets the authentication mechanisms supported by the mail server. + /// + /// + /// The authentication mechanisms are queried as part of the + /// Connect method. + /// + /// The authentication mechanisms. + public abstract HashSet AuthenticationMechanisms { + get; + } + + /// + /// Gets whether or not the client is currently connected to an mail server. + /// + /// + ///The state is set to true immediately after + /// one of the Connect + /// methods succeeds and is not set back to false until either the client + /// is disconnected via or until a + /// is thrown while attempting to read or write to + /// the underlying network socket. + /// When an is caught, the connection state of the + /// should be checked before continuing. + /// + /// true if the client is connected; otherwise, false. + public abstract bool IsConnected { + get; + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// true if the connection is secure; otherwise, false. + public abstract bool IsSecure { + get; + } + + /// + /// Get whether or not the client is currently authenticated with the mail server. + /// + /// + /// Gets whether or not the client is currently authenticated with the mail server. + /// To authenticate with the mail server, use one of the + /// Authenticate methods + /// or any of the Async alternatives. + /// + /// true if the client is authenticated; otherwise, false. + public abstract bool IsAuthenticated { + get; + } + + /// + /// Gets or sets the timeout for network streaming operations, in milliseconds. + /// + /// + /// Gets or sets the underlying socket stream's + /// and values. + /// + /// The timeout in milliseconds. + public abstract int Timeout { + get; set; + } + + const string AppleCertificateIssuer = "C=US, O=Apple Inc., OU=Certification Authority, CN=Apple IST CA 2 - G1"; + const string GMailCertificateIssuer = "CN=GTS CA 1O1, O=Google Trust Services, C=US"; + const string OutlookCertificateIssuer = "CN=DigiCert Cloud Services CA-1, O=DigiCert Inc, C=US"; + const string YahooCertificateIssuer = "CN=DigiCert SHA2 High Assurance Server CA, OU=www.digicert.com, O=DigiCert Inc, C=US"; + const string GmxCertificateIssuer = "CN=TeleSec ServerPass Extended Validation Class 3 CA, STREET=Untere Industriestr. 20, L=Netphen, OID.2.5.4.17=57250, S=Nordrhein Westfalen, OU=T-Systems Trust Center, O=T-Systems International GmbH, C=DE"; + + static bool IsKnownMailServerCertificate (X509Certificate2 certificate) + { + var cn = certificate.GetNameInfo (X509NameType.SimpleName, false); + var fingerprint = certificate.Thumbprint; + var serial = certificate.SerialNumber; + var issuer = certificate.Issuer; + + switch (cn) { + case "imap.gmail.com": + return issuer == GMailCertificateIssuer && serial == "0096768414983DDE9C0800000000320A68" && fingerprint == "A53BA86C137D828618540738014F7C3D52F699C7"; + case "pop.gmail.com": + return issuer == GMailCertificateIssuer && serial == "00D80446EA4406BA970800000000320A6A" && fingerprint == "379A18659C855AE5CD00E24CEBE2C6552235B701"; + case "smtp.gmail.com": + return issuer == GMailCertificateIssuer && serial == "00A2683EEFC8500CA20800000000320A71" && fingerprint == "8F0A0B43DE223D360C4BBC41725C202B806CED32"; + case "outlook.com": + return issuer == OutlookCertificateIssuer && serial == "0654F84B6325595A20BC68A6A5851CBB" && fingerprint == "7F0804B4D0A6C83E46A3A00EC98F8343D7308566"; + case "imap.mail.me.com": + return issuer == AppleCertificateIssuer && serial == "62CBBFC566127C4758E96BDBC38EC9E6" && fingerprint == "E1A5F9D22A810979CACDFC0B4151F561E8D02976"; + case "smtp.mail.me.com": + return issuer == AppleCertificateIssuer && serial == "3460D64A763D9ACA4B460C25021653C7" && fingerprint == "C262F01E83D6CE0C361E8B049E5BE8FE6E55806B"; + case "*.imap.mail.yahoo.com": + return issuer == YahooCertificateIssuer && serial == "0B2804C9ED82D14FEFEF111E54A0551C" && fingerprint == "F8047F0F60C4641F718353BE7DDC31665B96B5C0"; + case "legacy.pop.mail.yahoo.com": + return issuer == YahooCertificateIssuer && serial == "05179AA3E07FA5B4D0FC55A7A950B8D8" && fingerprint == "08E010CBAEFAADD20DB0B222C8B6812E762F28EC"; + case "smtp.mail.yahoo.com": + return issuer == YahooCertificateIssuer && serial == "0F962C48837807B6556C5B6961FC4671" && fingerprint == "E53995EBA816FB73FD4F4BD55ABED04981DA0F18"; + case "mail.gmx.net": + return issuer == GmxCertificateIssuer && serial == "218296213149726650EB233346353EEA" && fingerprint == "67DED57393303E005937D5EDECB6A29C136024CA"; + default: + return false; + } + } + + static bool IsUntrustedRoot (X509Chain chain) + { + foreach (var status in chain.ChainStatus) { + if (status.Status == X509ChainStatusFlags.NoError || status.Status == X509ChainStatusFlags.UntrustedRoot) + continue; + + return false; + } + + return true; + } + + internal SslCertificateValidationInfo SslCertificateValidationInfo; + + /// + /// The default server certificate validation callback used when connecting via SSL or TLS. + /// + /// + /// The default server certificate validation callback recognizes and accepts the certificates + /// for a list of commonly used mail servers such as gmail.com, outlook.com, mail.me.com, yahoo.com, + /// and gmx.net. + /// + /// true if the certificate is deemed valid; otherwise, false. + /// The object that is connecting via SSL or TLS. + /// The server's SSL certificate. + /// The server's SSL certificate chain. + /// The SSL policy errors. + protected bool DefaultServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + const SslPolicyErrors mask = SslPolicyErrors.RemoteCertificateNotAvailable | SslPolicyErrors.RemoteCertificateNameMismatch; + + SslCertificateValidationInfo = null; + + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + if ((sslPolicyErrors & mask) == 0) { + // At this point, all that is left is SslPolicyErrors.RemoteCertificateChainErrors + + // If the problem is an untrusted root, then compare the certificate to a list of known mail server certificates. + if (IsUntrustedRoot (chain) && certificate is X509Certificate2 certificate2) { + if (IsKnownMailServerCertificate (certificate2)) + return true; + } + } + + SslCertificateValidationInfo = new SslCertificateValidationInfo (sender, certificate, chain, sslPolicyErrors); + + return false; + } + + internal async Task ConnectSocket (string host, int port, bool doAsync, CancellationToken cancellationToken) + { + if (ProxyClient != null) { + ProxyClient.LocalEndPoint = LocalEndPoint; + + if (doAsync) + return await ProxyClient.ConnectAsync (host, port, Timeout, cancellationToken).ConfigureAwait (false); + + return ProxyClient.Connect (host, port, Timeout, cancellationToken); + } + + return await SocketUtils.ConnectAsync (host, port, LocalEndPoint, Timeout, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Establish a connection to the specified mail server. + /// + /// + /// Establishes a connection to the specified mail server. + /// + /// + /// + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server. + /// + /// + /// Asynchronously establishes a connection to the specified mail server. + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Establish a connection to the specified mail server using the provided socket. + /// + /// + /// Establish a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided socket. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided socket. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Establish a connection to the specified mail server using the provided stream. + /// + /// + /// Establish a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously establish a connection to the specified mail server using the provided stream. + /// + /// + /// Asynchronously establishes a connection to the specified mail server using the provided stream. + /// If a successful connection is made, the + /// property will be populated. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)); + + internal SecureSocketOptions GetSecureSocketOptions (Uri uri) + { + var query = uri.ParsedQuery (); + var protocol = uri.Scheme; + string value; + + // Note: early versions of MailKit used "pop3" and "pop3s" + if (protocol.Equals ("pop3s", StringComparison.OrdinalIgnoreCase)) + protocol = "pops"; + else if (protocol.Equals ("pop3", StringComparison.OrdinalIgnoreCase)) + protocol = "pop"; + + if (protocol.Equals (Protocol + "s", StringComparison.OrdinalIgnoreCase)) + return SecureSocketOptions.SslOnConnect; + + if (!protocol.Equals (Protocol, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException ("Unknown URI scheme.", nameof (uri)); + + if (query.TryGetValue ("starttls", out value)) { + switch (value.ToLowerInvariant ()) { + default: + return SecureSocketOptions.StartTlsWhenAvailable; + case "always": case "true": case "yes": + return SecureSocketOptions.StartTls; + case "never": case "false": case "no": + return SecureSocketOptions.None; + } + } + + return SecureSocketOptions.StartTlsWhenAvailable; + } + + /// + /// Establish a connection to the specified mail server. + /// + /// + /// Establishes a connection to the specified mail server. + /// + /// + /// + /// + /// The server URI. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// The is not an absolute URI. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public void Connect (Uri uri, CancellationToken cancellationToken = default (CancellationToken)) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + if (!uri.IsAbsoluteUri) + throw new ArgumentException ("The uri must be absolute.", nameof (uri)); + + var options = GetSecureSocketOptions (uri); + + Connect (uri.Host, uri.Port < 0 ? 0 : uri.Port, options, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified mail server. + /// + /// + /// Asynchronously establishes a connection to the specified mail server. + /// + /// An asynchronous task context. + /// The server URI. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// The is not an absolute URI. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public Task ConnectAsync (Uri uri, CancellationToken cancellationToken = default (CancellationToken)) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + if (!uri.IsAbsoluteUri) + throw new ArgumentException ("The uri must be absolute.", nameof (uri)); + + var options = GetSecureSocketOptions (uri); + + return ConnectAsync (uri.Host, uri.Port < 0 ? 0 : uri.Port, options, cancellationToken); + } + + /// + /// Establish a connection to the specified mail server. + /// + /// + /// Establishes a connection to the specified mail server. + /// + /// The argument only controls whether or + /// not the client makes an SSL-wrapped connection. In other words, even if the + /// parameter is false, SSL/TLS may still be used if + /// the mail server supports the STARTTLS extension. + /// To disable all use of SSL/TLS, use the + /// + /// overload with a value of + /// SecureSocketOptions.None + /// instead. + /// + /// + /// The host to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// is out of range (0 to 65535, inclusive). + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public void Connect (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + Connect (host, port, useSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified mail server. + /// + /// + /// Asynchronously establishes a connection to the specified mail server. + /// + /// The argument only controls whether or + /// not the client makes an SSL-wrapped connection. In other words, even if the + /// parameter is false, SSL/TLS may still be used if + /// the mail server supports the STARTTLS extension. + /// To disable all use of SSL/TLS, use the + /// + /// overload with a value of + /// SecureSocketOptions.None + /// instead. + /// + /// + /// An asynchronous task context. + /// The host to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// true if the client should make an SSL-wrapped connection to the server; otherwise, false. + /// The cancellation token. + /// + /// The is null. + /// + /// + /// is out of range (0 to 65535, inclusive). + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public Task ConnectAsync (string host, int port, bool useSsl, CancellationToken cancellationToken = default (CancellationToken)) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + return ConnectAsync (host, port, useSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable, cancellationToken); + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public void Authenticate (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + Authenticate (Encoding.UTF8, credentials, cancellationToken); + } + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public Task AuthenticateAsync (ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (Encoding.UTF8, credentials, cancellationToken); + } + + /// + /// Authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public void Authenticate (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var credentials = new NetworkCredential (userName, password); + + Authenticate (encoding, credentials, cancellationToken); + } + + /// + /// Asynchronously authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public Task AuthenticateAsync (Encoding encoding, string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var credentials = new NetworkCredential (userName, password); + + return AuthenticateAsync (encoding, credentials, cancellationToken); + } + + /// + /// Authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// + /// + /// + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public void Authenticate (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + { + Authenticate (Encoding.UTF8, userName, password, cancellationToken); + } + + /// + /// Asynchronously authenticate using the specified user name and password. + /// + /// + /// If the server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the default login command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The user name. + /// The password. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public Task AuthenticateAsync (string userName, string password, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (Encoding.UTF8, userName, password, cancellationToken); + } + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected or is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + public abstract Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Disconnect the service. + /// + /// + /// If is true, a logout/quit command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public abstract void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is true, a logout/quit command will be issued in order to disconnect cleanly. + /// + /// An asynchronous task context. + /// If set to true, a logout/quit command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public abstract Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Ping the mail server to keep the connection alive. + /// + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// -or- + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract void NoOp (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously ping the mail server to keep the connection alive. + /// + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// -or- + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command was rejected by the mail server. + /// + /// + /// The server responded with an unexpected token. + /// + public abstract Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when the client has been successfully connected. + /// + /// + /// The event is raised when the client + /// successfully connects to the mail server. + /// + public event EventHandler Connected; + + /// + /// Raise the connected event. + /// + /// + /// Raises the connected event. + /// + /// The name of the host that the client connected to. + /// The port that the client connected to on the remote host. + /// The SSL/TLS options that were used when connecting. + protected virtual void OnConnected (string host, int port, SecureSocketOptions options) + { + var handler = Connected; + + if (handler != null) + handler (this, new ConnectedEventArgs (host, port, options)); + } + + /// + /// Occurs when the client gets disconnected. + /// + /// + /// The event is raised whenever the client + /// gets disconnected. + /// + public event EventHandler Disconnected; + + /// + /// Raise the disconnected event. + /// + /// + /// Raises the disconnected event. + /// + /// The name of the host that the client was connected to. + /// The port that the client was connected to on the remote host. + /// The SSL/TLS options that were used by the client. + /// true if the disconnect was explicitly requested; otherwise, false. + protected virtual void OnDisconnected (string host, int port, SecureSocketOptions options, bool requested) + { + var handler = Disconnected; + + if (handler != null) + handler (this, new DisconnectedEventArgs (host, port, options, requested)); + } + + /// + /// Occurs when the client has been successfully authenticated. + /// + /// + /// The event is raised whenever the client + /// has been authenticated. + /// + public event EventHandler Authenticated; + + /// + /// Raise the authenticated event. + /// + /// + /// Raises the authenticated event. + /// + /// The notification sent by the server when the client successfully authenticates. + protected virtual void OnAuthenticated (string message) + { + var handler = Authenticated; + + if (handler != null) + handler (this, new AuthenticatedEventArgs (message)); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected virtual void Dispose (bool disposing) + { + if (disposing) + ProtocolLogger.Dispose (); + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + } +} diff --git a/src/MailKit/MailSpool.cs b/src/MailKit/MailSpool.cs new file mode 100644 index 0000000..17c0974 --- /dev/null +++ b/src/MailKit/MailSpool.cs @@ -0,0 +1,1588 @@ +// +// MailSpool.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// An abstract mail spool implementation. + /// + /// + /// An abstract mail spool implementation. + /// + public abstract class MailSpool : MailService, IMailSpool + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The protocol logger. + /// + /// is null. + /// + protected MailSpool (IProtocolLogger protocolLogger) : base (protocolLogger) + { + } + + /// + /// Get the number of messages available in the message spool. + /// + /// + /// Gets the number of messages available in the message spool. + /// Once authenticated, the property will be set + /// to the number of available messages in the spool. + /// + /// + /// + /// + /// The message count. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public abstract int Count { + get; + } + + /// + /// Get whether or not the service supports referencing messages by UIDs. + /// + /// + /// Not all servers support referencing messages by UID, so this property should + /// be checked before using + /// and . + /// If the server does not support UIDs, then all methods that take UID arguments + /// along with and + /// will fail. + /// + /// true if supports uids; otherwise, false. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public abstract bool SupportsUids { + get; + } + + /// + /// Get the message count. + /// + /// + /// Gets the message count. + /// + /// The message count. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the message count. + /// + /// + /// Asynchronously gets the message count. + /// + /// The message count. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetMessageCountAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the UID of the message at the specified index. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail spool does not support UIDs. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the UID of the message at the specified index. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail spool does not support UIDs. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the full list of available message UIDs. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// + /// + /// + /// The message uids. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail spool does not support UIDs. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the full list of available message UIDs. + /// + /// + /// Not all servers support UIDs, so you should first check + /// the property. + /// + /// The message uids. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The mail spool does not support UIDs. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessageUidsAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the size of the specified message, in bytes. + /// + /// + /// Gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the size of the specified message, in bytes. + /// + /// + /// Asynchronously gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the sizes for all available messages, in bytes. + /// + /// + /// Gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the sizes for all available messages, in bytes. + /// + /// + /// Asynchronously gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessageSizesAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers for the specified message. + /// + /// + /// Gets the headers for the specified message. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the headers for the specified message. + /// + /// + /// Asynchronously gets the headers for the specified message. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers for the specified messages. + /// + /// + /// Gets the headers for the specified messages. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the headers for the specified messages. + /// + /// + /// Asynchronously gets the headers for the specified messages. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the message at the specified index. + /// + /// + /// Gets the message at the specified index. + /// + /// + /// + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message at the specified index. + /// + /// + /// Asynchronously gets the message at the specified index. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the messages at the specified indexes. + /// + /// + /// Get the messages at the specified indexes. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the messages at the specified indexes. + /// + /// + /// Asynchronously get the messages at the specified indexes. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the messages within the specified range. + /// + /// + /// Gets the messages within the specified range. + /// + /// + /// + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the messages within the specified range. + /// + /// + /// Asynchronously gets the messages within the specified range. + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header stream at the specified index. + /// + /// + /// Gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header stream at the specified index. + /// + /// + /// Asynchronously gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header streams at the specified indexes. + /// + /// + /// Get the message or header streams at the specified indexes. + /// If the mail server supports pipelining, this method will likely be more + /// efficient than using for + /// each message because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header streams at the specified indexes. + /// + /// + /// Asynchronously get the message or header streams at the specified indexes. + /// + /// The messages. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the message or header streams within the specified range. + /// + /// + /// Gets the message or header streams within the specified range. + /// If the mail server supports pipelining, this method will likely be more + /// efficient than using for + /// each message because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the message or header streams within the specified range. + /// + /// + /// Asynchronously gets the message or header streams within the specified range. + /// + /// The messages. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// -or- + /// No indexes were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract void Reset (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract Task ResetAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A command failed. + /// + /// + /// A protocol error occurred. + /// + public abstract IEnumerator GetEnumerator (); + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A command failed. + /// + /// + /// A protocol error occurred. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + } +} diff --git a/src/MailKit/MailStore.cs b/src/MailKit/MailStore.cs new file mode 100644 index 0000000..bc8d8bf --- /dev/null +++ b/src/MailKit/MailStore.cs @@ -0,0 +1,872 @@ +// +// MailStore.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit { + /// + /// An abstract mail store implementation. + /// + /// + /// An abstract mail store implementation. + /// + public abstract class MailStore : MailService, IMailStore + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The protocol logger. + /// + /// is null. + /// + protected MailStore (IProtocolLogger protocolLogger) : base (protocolLogger) + { + } + + /// + /// Gets the personal namespaces. + /// + /// + /// The personal folder namespaces contain a user's personal mailbox folders. + /// + /// The personal namespaces. + public abstract FolderNamespaceCollection PersonalNamespaces { + get; + } + + /// + /// Gets the shared namespaces. + /// + /// + /// The shared folder namespaces contain mailbox folders that are shared with the user. + /// + /// The shared namespaces. + public abstract FolderNamespaceCollection SharedNamespaces { + get; + } + + /// + /// Gets the other namespaces. + /// + /// + /// The other folder namespaces contain other mailbox folders. + /// + /// The other namespaces. + public abstract FolderNamespaceCollection OtherNamespaces { + get; + } + + /// + /// Get whether or not the mail store supports quotas. + /// + /// + /// Gets whether or not the mail store supports quotas. + /// + /// true if the mail store supports quotas; otherwise, false. + public abstract bool SupportsQuotas { + get; + } + + /// + /// Get the threading algorithms supported by the mail store. + /// + /// + /// The threading algorithms are queried as part of the + /// Connect + /// and Authenticate methods. + /// + /// + /// + /// + /// The supported threading algorithms. + public abstract HashSet ThreadingAlgorithms { + get; + } + + /// + /// Get the Inbox folder. + /// + /// + /// The Inbox folder is the default folder and always exists on the mail store. + /// This property will only be available after the client has been authenticated. + /// + /// The Inbox folder. + public abstract IMailFolder Inbox { + get; + } + + /// + /// Enable the quick resynchronization feature. + /// + /// + /// Enables quick resynchronization when a folder is opened using the + /// + /// method. + /// If this feature is enabled, the event is replaced + /// with the event. + /// This method needs to be called immediately after calling one of the + /// Authenticate methods, before + /// opening any folders. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The mail store does not support quick resynchronization. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously enable the quick resynchronization feature. + /// + /// + /// Enables quick resynchronization when a folder is opened using the + /// + /// method. + /// If this feature is enabled, the event is replaced + /// with the event. + /// This method needs to be called immediately after calling one of the + /// Authenticate methods, before + /// opening any folders. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The mail store does not support quick resynchronization. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract Task EnableQuickResyncAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the specified special folder. + /// + /// + /// Not all mail stores support special folders. Each implementation + /// should provide a way to determine if special folders are supported. + /// + /// The folder if available; otherwise null. + /// The type of special folder. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public abstract IMailFolder GetFolder (SpecialFolder folder); + + /// + /// Get the folder for the specified namespace. + /// + /// + /// Gets the folder for the specified namespace. + /// + /// The folder. + /// The namespace. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder could not be found. + /// + public abstract IMailFolder GetFolder (FolderNamespace @namespace); + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public virtual IList GetFolders (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetFolders (@namespace, StatusItems.None, subscribedOnly, cancellationToken); + } + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Asynchronously gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public virtual Task> GetFoldersAsync (FolderNamespace @namespace, bool subscribedOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetFoldersAsync (@namespace, StatusItems.None, subscribedOnly, cancellationToken); + } + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Asynchronously gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the folder for the specified path. + /// + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the folder for the specified path. + /// + /// + /// Asynchronously gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// A protocol error occurred. + /// + /// + /// The command failed. + /// + public abstract Task GetFolderAsync (string path, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual MetadataCollection GetMetadata (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadata (new MetadataOptions (), tags, cancellationToken); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public virtual Task GetMetadataAsync (IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (new MetadataOptions (), tags, cancellationToken); + } + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Asynchronously gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sets the specified metadata. + /// + /// + /// Asynchronously sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder does not support metadata. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public abstract Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Occurs when a remote message store receives an alert message from the server. + /// + /// + /// The event is raised whenever the mail server sends an + /// alert message. + /// + public event EventHandler Alert; + + /// + /// Raise the alert event. + /// + /// + /// Raises the alert event. + /// + /// The alert message. + /// + /// is null. + /// + protected virtual void OnAlert (string message) + { + var handler = Alert; + + if (handler != null) + handler (this, new AlertEventArgs (message)); + } + + /// + /// Occurs when a folder is created. + /// + /// + /// The event is emitted when a new folder is created. + /// + public event EventHandler FolderCreated; + + /// + /// Raise the folder created event. + /// + /// + /// Raises the folder created event. + /// + /// The folder that was just created. + protected virtual void OnFolderCreated (IMailFolder folder) + { + var handler = FolderCreated; + + if (handler != null) + handler (this, new FolderCreatedEventArgs (folder)); + } + + /// + /// Occurs when metadata changes. + /// + /// + /// The event is emitted when metadata changes. + /// + public event EventHandler MetadataChanged; + + /// + /// Raise the metadata changed event. + /// + /// + /// Raises the metadata changed event. + /// + /// The metadata that changed. + protected virtual void OnMetadataChanged (Metadata metadata) + { + var handler = MetadataChanged; + + if (handler != null) + handler (this, new MetadataChangedEventArgs (metadata)); + } + } +} diff --git a/src/MailKit/MailTransport.cs b/src/MailKit/MailTransport.cs new file mode 100644 index 0000000..418d2b9 --- /dev/null +++ b/src/MailKit/MailTransport.cs @@ -0,0 +1,507 @@ +// +// MailTransport.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit { + /// + /// An abstract mail transport implementation. + /// + /// + /// An abstract mail transport implementation. + /// + public abstract class MailTransport : MailService, IMailTransport + { + static readonly FormatOptions DefaultOptions; + + static MailTransport () + { + var options = FormatOptions.Default.Clone (); + options.HiddenHeaders.Add (HeaderId.ContentLength); + options.HiddenHeaders.Add (HeaderId.ResentBcc); + options.HiddenHeaders.Add (HeaderId.Bcc); + options.NewLineFormat = NewLineFormat.Dos; + + DefaultOptions = options; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The protocol logger. + /// + /// is null. + /// + protected MailTransport (IProtocolLogger protocolLogger) : base (protocolLogger) + { + } + + /// + /// Send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// + /// + /// + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public virtual void Send (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + Send (DefaultOptions, message, cancellationToken, progress); + } + + /// + /// Asynchronously send the specified message. + /// + /// + /// Asynchronously sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// An asynchronous task context. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public virtual Task SendAsync (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return SendAsync (DefaultOptions, message, cancellationToken, progress); + } + + /// + /// Send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the specified message using the supplied sender and recipients. + /// + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public virtual void Send (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + Send (DefaultOptions, message, sender, recipients, cancellationToken, progress); + } + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Asynchronously sends the specified message using the supplied sender and recipients. + /// + /// An asynchronous task context. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public virtual Task SendAsync (MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return SendAsync (DefaultOptions, message, sender, recipients, cancellationToken, progress); + } + + /// + /// Send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// + /// + /// + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// Internationalized formatting was requested but is not supported by the transport. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public abstract void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message. + /// + /// + /// Asynchronously sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// Internationalized formatting was requested but is not supported by the transport. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public abstract Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the specified message using the supplied sender and recipients. + /// + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// Internationalized formatting was requested but is not supported by the transport. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public abstract void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Asynchronously sends the specified message using the supplied sender and recipients. + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The operation has been canceled. + /// + /// + /// Internationalized formatting was requested but is not supported by the transport. + /// + /// + /// An I/O error occurred. + /// + /// + /// The send command failed. + /// + /// + /// A protocol exception occurred. + /// + public abstract Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Occurs when a message is successfully sent via the transport. + /// + /// + /// The event will be emitted each time a message is successfully sent. + /// + public event EventHandler MessageSent; + + /// + /// Raise the message sent event. + /// + /// + /// Raises the message sent event. + /// + /// The message sent event args. + protected virtual void OnMessageSent (MessageSentEventArgs e) + { + var handler = MessageSent; + + if (handler != null) + handler (this, e); + } + } +} diff --git a/src/MailKit/MessageEventArgs.cs b/src/MailKit/MessageEventArgs.cs new file mode 100644 index 0000000..6379c10 --- /dev/null +++ b/src/MailKit/MessageEventArgs.cs @@ -0,0 +1,98 @@ +// +// MessageEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when the state of a message changes. + /// + /// + /// Event args used when the state of a message changes. + /// + public class MessageEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// + /// is out of range. + /// + public MessageEventArgs (int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException (nameof (index)); + + Index = index; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// + /// is out of range. + /// + public MessageEventArgs (int index, UniqueId uid) + { + if (index < 0) + throw new ArgumentOutOfRangeException (nameof (index)); + + Index = index; + UniqueId = uid; + } + + /// + /// Gets the index of the message that changed. + /// + /// + /// Gets the index of the message that changed. + /// + /// The index of the message. + public int Index { + get; private set; + } + + /// + /// Gets the unique ID of the message that changed, if available. + /// + /// + /// Gets the unique ID of the message that changed, if available. + /// + /// The unique ID of the message. + public UniqueId? UniqueId { + get; internal set; + } + } +} diff --git a/src/MailKit/MessageFlags.cs b/src/MailKit/MessageFlags.cs new file mode 100644 index 0000000..3c80c0b --- /dev/null +++ b/src/MailKit/MessageFlags.cs @@ -0,0 +1,78 @@ +// +// MessageFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// An enumeration of message flags. + /// + /// + /// An enumeration of message flags. + /// + [Flags] + public enum MessageFlags { + /// + /// No message flags are set. + /// + None = 0, + + /// + /// The message has been read. + /// + Seen = 1 << 0, + + /// + /// The message has been answered (replied to). + /// + Answered = 1 << 1, + + /// + /// The message has been flagged for importance. + /// + Flagged = 1 << 2, + + /// + /// The message has been marked for deletion. + /// + Deleted = 1 << 3, + + /// + /// The message is marked as a draft. + /// + Draft = 1 << 4, + + /// + /// The message has just recently arrived in the folder. + /// + Recent = 1 << 5, + + /// + /// User-defined flags are allowed by the folder. + /// + UserDefined = 1 << 6, + } +} diff --git a/src/MailKit/MessageFlagsChangedEventArgs.cs b/src/MailKit/MessageFlagsChangedEventArgs.cs new file mode 100644 index 0000000..868cb7a --- /dev/null +++ b/src/MailKit/MessageFlagsChangedEventArgs.cs @@ -0,0 +1,269 @@ +// +// MessageFlagsChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + /// + /// Event args for the event. + /// + /// + /// Event args for the event. + /// + public class MessageFlagsChangedEventArgs : MessageEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + internal MessageFlagsChangedEventArgs (int index) : base (index) + { + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message flags. + public MessageFlagsChangedEventArgs (int index, MessageFlags flags) : base (index) + { + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message flags. + /// The user-defined message flags. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet keywords) : base (index) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message flags. + /// The modification sequence value. + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, MessageFlags flags, ulong modseq) : base (index) + { + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + ModSeq = modseq; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message flags. + /// The user-defined message flags. + /// The modification sequence value. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, MessageFlags flags, HashSet keywords, ulong modseq) : base (index) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords; + ModSeq = modseq; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message flags. + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags) : base (index, uid) + { + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message flags. + /// The user-defined message flags. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, HashSet keywords) : base (index, uid) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message flags. + /// The modification sequence value. + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, ulong modseq) : base (index, uid) + { + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + ModSeq = modseq; + Flags = flags; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message flags. + /// The user-defined message flags. + /// The modification sequence value. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageFlagsChangedEventArgs (int index, UniqueId uid, MessageFlags flags, HashSet keywords, ulong modseq) : base (index, uid) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + Keywords = keywords; + ModSeq = modseq; + Flags = flags; + } + + /// + /// Gets the updated message flags. + /// + /// + /// Gets the updated message flags. + /// + /// The updated message flags. + public MessageFlags Flags { + get; internal set; + } + + /// + /// Gets the updated user-defined message flags. + /// + /// + /// Gets the updated user-defined message flags. + /// + /// The updated user-defined message flags. + public HashSet Keywords { + get; private set; + } + + /// + /// Gets the updated user-defined message flags. + /// + /// + /// Gets the updated user-defined message flags. + /// + /// The updated user-defined message flags. + [Obsolete ("Use Keywords instead.")] + public HashSet UserFlags { + get { return Keywords; } + } + + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// The mod-sequence value. + public ulong? ModSeq { + get; internal set; + } + } +} diff --git a/src/MailKit/MessageLabelsChangedEventArgs.cs b/src/MailKit/MessageLabelsChangedEventArgs.cs new file mode 100644 index 0000000..2498af3 --- /dev/null +++ b/src/MailKit/MessageLabelsChangedEventArgs.cs @@ -0,0 +1,170 @@ +// +// LabelsChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MailKit { + /// + /// Event args for the event. + /// + /// + /// Event args for the event. + /// + public class MessageLabelsChangedEventArgs : MessageEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// + /// is out of range. + /// + internal MessageLabelsChangedEventArgs (int index) : base (index) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message labels. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageLabelsChangedEventArgs (int index, IList labels) : base (index) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + Labels = new ReadOnlyCollection (labels); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The message labels. + /// The modification sequence value. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageLabelsChangedEventArgs (int index, IList labels, ulong modseq) : base (index) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + Labels = new ReadOnlyCollection (labels); + ModSeq = modseq; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message labels. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels) : base (index, uid) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + Labels = new ReadOnlyCollection (labels); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The message labels. + /// The modification sequence value. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MessageLabelsChangedEventArgs (int index, UniqueId uid, IList labels, ulong modseq) : base (index, uid) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + Labels = new ReadOnlyCollection (labels); + ModSeq = modseq; + } + + /// + /// Gets the updated labels. + /// + /// + /// Gets the updated labels. + /// + /// The updated labels. + public IList Labels { + get; internal set; + } + + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// + /// Gets the updated mod-sequence value of the message, if available. + /// + /// The mod-sequence value. + public ulong? ModSeq { + get; internal set; + } + } +} diff --git a/src/MailKit/MessageNotFoundException.cs b/src/MailKit/MessageNotFoundException.cs new file mode 100644 index 0000000..d7ca582 --- /dev/null +++ b/src/MailKit/MessageNotFoundException.cs @@ -0,0 +1,92 @@ +// +// MessageNotFoundException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when a message (or body part) could not be found. + /// + /// + /// This exception is thrown by methods such as + /// IMailFolder.GetMessage, + /// IMailFolder.GetBodyPart, or + /// IMailFolder.GetStream + /// when the server's response does not contain the message, body part, or stream data requested. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class MessageNotFoundException : Exception + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected MessageNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The inner exception. + /// + /// + public MessageNotFoundException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public MessageNotFoundException (string message) : base (message) + { + } + } +} diff --git a/src/MailKit/MessageSentEventArgs.cs b/src/MailKit/MessageSentEventArgs.cs new file mode 100644 index 0000000..effeed8 --- /dev/null +++ b/src/MailKit/MessageSentEventArgs.cs @@ -0,0 +1,87 @@ +// +// MessageSentEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit; + +namespace MailKit { + /// + /// Event args used when a message is successfully sent. + /// + /// + /// Event args used when message is successfully sent. + /// + public class MessageSentEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message that was just sent. + /// The response from the server. + /// + /// is null. + /// -or- + /// is null. + /// + public MessageSentEventArgs (MimeMessage message, string response) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (response == null) + throw new ArgumentNullException (nameof (response)); + + Message = message; + Response = response; + } + + /// + /// Get the message that was just sent. + /// + /// + /// Gets the message that was just sent. + /// + /// The message. + public MimeMessage Message { + get; private set; + } + + /// + /// Get the server's response. + /// + /// + /// Gets the server's response. + /// + /// The response. + public string Response { + get; private set; + } + } +} diff --git a/src/MailKit/MessageSorter.cs b/src/MailKit/MessageSorter.cs new file mode 100644 index 0000000..f967bef --- /dev/null +++ b/src/MailKit/MessageSorter.cs @@ -0,0 +1,295 @@ +// +// MessageSorter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Collections.Generic; + +using MimeKit; +using MailKit.Search; + +namespace MailKit { + /// + /// Routines for sorting messages. + /// + /// + /// Routines for sorting messages. + /// + public static class MessageSorter + { + class MessageComparer : IComparer where T : IMessageSummary + { + readonly IList orderBy; + + public MessageComparer (IList orderBy) + { + this.orderBy = orderBy; + } + + #region IComparer implementation + + static int CompareDisplayNames (InternetAddressList list1, InternetAddressList list2) + { + var m1 = list1.Mailboxes.GetEnumerator (); + var m2 = list2.Mailboxes.GetEnumerator (); + bool n1 = m1.MoveNext (); + bool n2 = m2.MoveNext (); + + while (n1 && n2) { + var name1 = m1.Current.Name ?? string.Empty; + var name2 = m2.Current.Name ?? string.Empty; + int cmp; + + if ((cmp = string.Compare (name1, name2, StringComparison.OrdinalIgnoreCase)) != 0) + return cmp; + + n1 = m1.MoveNext (); + n2 = m2.MoveNext (); + } + + return n1 ? 1 : (n2 ? -1 : 0); + } + + static int CompareMailboxAddresses (InternetAddressList list1, InternetAddressList list2) + { + var m1 = list1.Mailboxes.GetEnumerator (); + var m2 = list2.Mailboxes.GetEnumerator (); + bool n1 = m1.MoveNext (); + bool n2 = m2.MoveNext (); + + while (n1 && n2) { + int cmp; + + if ((cmp = string.Compare (m1.Current.Address, m2.Current.Address, StringComparison.OrdinalIgnoreCase)) != 0) + return cmp; + + n1 = m1.MoveNext (); + n2 = m2.MoveNext (); + } + + return n1 ? 1 : (n2 ? -1 : 0); + } + + public int Compare (T x, T y) + { + int cmp = 0; + + for (int i = 0; i < orderBy.Count; i++) { + switch (orderBy[i].Type) { + case OrderByType.Annotation: + var annotation = (OrderByAnnotation) orderBy[i]; + + var xannotation = x.Annotations?.FirstOrDefault (a => a.Entry == annotation.Entry); + var yannotation = y.Annotations?.FirstOrDefault (a => a.Entry == annotation.Entry); + + var xvalue = xannotation?.Properties[annotation.Attribute] ?? string.Empty; + var yvalue = yannotation?.Properties[annotation.Attribute] ?? string.Empty; + + cmp = string.Compare (xvalue, yvalue, StringComparison.OrdinalIgnoreCase); + break; + case OrderByType.Arrival: + cmp = x.Index.CompareTo (y.Index); + break; + case OrderByType.Cc: + cmp = CompareMailboxAddresses (x.Envelope.Cc, y.Envelope.Cc); + break; + case OrderByType.Date: + cmp = x.Date.CompareTo (y.Date); + break; + case OrderByType.DisplayFrom: + cmp = CompareDisplayNames (x.Envelope.From, y.Envelope.From); + break; + case OrderByType.From: + cmp = CompareMailboxAddresses (x.Envelope.From, y.Envelope.From); + break; + case OrderByType.ModSeq: + var xmodseq = x.ModSeq ?? 0; + var ymodseq = y.ModSeq ?? 0; + + cmp = xmodseq.CompareTo (ymodseq); + break; + case OrderByType.Size: + var xsize = x.Size ?? 0; + var ysize = y.Size ?? 0; + + cmp = xsize.CompareTo (ysize); + break; + case OrderByType.Subject: + var xsubject = x.Envelope.Subject ?? string.Empty; + var ysubject = y.Envelope.Subject ?? string.Empty; + + cmp = string.Compare (xsubject, ysubject, StringComparison.OrdinalIgnoreCase); + break; + case OrderByType.DisplayTo: + cmp = CompareDisplayNames (x.Envelope.To, y.Envelope.To); + break; + case OrderByType.To: + cmp = CompareMailboxAddresses (x.Envelope.To, y.Envelope.To); + break; + } + + if (cmp == 0) + continue; + + return orderBy[i].Order == SortOrder.Descending ? cmp * -1 : cmp; + } + + return cmp; + } + + #endregion + } + + static MessageSummaryItems GetMessageSummaryItems (IList orderBy) + { + var items = MessageSummaryItems.None; + + for (int i = 0; i < orderBy.Count; i++) { + switch (orderBy[i].Type) { + case OrderByType.Annotation: + items |= MessageSummaryItems.Annotations; + break; + case OrderByType.Arrival: + break; + case OrderByType.Cc: + case OrderByType.Date: + case OrderByType.DisplayFrom: + case OrderByType.DisplayTo: + case OrderByType.From: + case OrderByType.Subject: + case OrderByType.To: + items |= MessageSummaryItems.Envelope; + break; + case OrderByType.ModSeq: + items |= MessageSummaryItems.ModSeq; + break; + case OrderByType.Size: + items |= MessageSummaryItems.Size; + break; + } + } + + return items; + } + + /// + /// Sorts the messages by the specified ordering. + /// + /// + /// Sorts the messages by the specified ordering. + /// + /// The sorted messages. + /// The message items must implement the interface. + /// The messages to sort. + /// The sort ordering. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains one or more items that is missing information needed for sorting. + /// -or- + /// is an empty list. + /// + public static IList Sort (this IEnumerable messages, IList orderBy) where T : IMessageSummary + { + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + var requiredFields = GetMessageSummaryItems (orderBy); + var list = new List (); + + foreach (var message in messages) { + if ((message.Fields & requiredFields) != requiredFields) + throw new ArgumentException ("One or more messages is missing information needed for sorting.", nameof (messages)); + + list.Add (message); + } + + if (list.Count < 2) + return list; + + var comparer = new MessageComparer (orderBy); + + list.Sort (comparer); + + return list; + } + + /// + /// Sorts the messages by the specified ordering. + /// + /// + /// Sorts the messages by the specified ordering. + /// + /// The sorted messages. + /// The message items must implement the interface. + /// The messages to sort. + /// The sort ordering. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains one or more items that is missing information needed for sorting. + /// -or- + /// is an empty list. + /// + public static void Sort (this List messages, IList orderBy) where T : IMessageSummary + { + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + var requiredFields = GetMessageSummaryItems (orderBy); + + for (int i = 0; i < messages.Count; i++) { + if ((messages[i].Fields & requiredFields) != requiredFields) + throw new ArgumentException ("One or more messages is missing information needed for sorting.", nameof (messages)); + } + + if (messages.Count < 2) + return; + + var comparer = new MessageComparer (orderBy); + + messages.Sort (comparer); + } + } +} diff --git a/src/MailKit/MessageSummary.cs b/src/MailKit/MessageSummary.cs new file mode 100644 index 0000000..798a24d --- /dev/null +++ b/src/MailKit/MessageSummary.cs @@ -0,0 +1,758 @@ +// +// MessageSummary.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.Utils; + +namespace MailKit { + /// + /// A summary of a message. + /// + /// + /// The Fetch and + /// FetchAsync methods + /// return lists of items. + /// The properties of the that will be available + /// depend on the passed to the aformentioned method. + /// + public class MessageSummary : IMessageSummary + { + int threadableReplyDepth = -1; + string normalizedSubject; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// + /// is negative. + /// + public MessageSummary (int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException (nameof (index)); + + Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); + Index = index; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The folder that the message belongs to. + /// The message index. + /// + /// is null. + /// + /// + /// is negative. + /// + public MessageSummary (IMailFolder folder, int index) : this (index) + { + if (folder == null) + throw new ArgumentNullException (nameof (folder)); + + Folder = folder; + } + + void UpdateThreadableSubject () + { + if (normalizedSubject != null) + return; + + if (Envelope?.Subject != null) { + normalizedSubject = MessageThreader.GetThreadableSubject (Envelope.Subject, out threadableReplyDepth); + } else { + normalizedSubject = string.Empty; + threadableReplyDepth = 0; + } + } + + /// + /// Get the folder that the message belongs to. + /// + /// + /// Gets the folder that the message belongs to, if available. + /// + /// The folder. + public IMailFolder Folder { + get; private set; + } + + /// + /// Get a bitmask of fields that have been populated. + /// + /// + /// Gets a bitmask of fields that have been populated. + /// + /// The fields that have been populated. + public MessageSummaryItems Fields { + get; internal set; + } + + /// + /// Gets the body structure of the message, if available. + /// + /// + /// The body will be one of , + /// , , + /// or . + /// This property will only be set if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The body structure of the message. + public BodyPart Body { + get; set; + } + + static BodyPart GetMultipartRelatedRoot (BodyPartMultipart related) + { + string start = related.ContentType.Parameters["start"]; + string contentId; + + if (start == null) + return related.BodyParts.Count > 0 ? related.BodyParts[0] : null; + + if ((contentId = MimeUtils.EnumerateReferences (start).FirstOrDefault ()) == null) + contentId = start; + + var cid = new Uri (string.Format ("cid:{0}", contentId)); + + for (int i = 0; i < related.BodyParts.Count; i++) { + var basic = related.BodyParts[i] as BodyPartBasic; + + if (basic != null && (basic.ContentId == contentId || basic.ContentLocation == cid)) + return basic; + + var multipart = related.BodyParts[i] as BodyPartMultipart; + + if (multipart != null && multipart.ContentLocation == cid) + return multipart; + } + + return null; + } + + static bool TryGetMultipartAlternativeBody (BodyPartMultipart multipart, bool html, out BodyPartText body) + { + // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful + for (int i = multipart.BodyParts.Count - 1; i >= 0; i--) { + var multi = multipart.BodyParts[i] as BodyPartMultipart; + BodyPartText text = null; + + if (multi != null) { + if (multi.ContentType.IsMimeType ("multipart", "related")) { + text = GetMultipartRelatedRoot (multi) as BodyPartText; + } else if (multi.ContentType.IsMimeType ("multipart", "alternative")) { + // Note: nested multipart/alternatives make no sense... yet here we are. + if (TryGetMultipartAlternativeBody (multi, html, out body)) + return true; + } + } else { + text = multipart.BodyParts[i] as BodyPartText; + } + + if (text != null && (html ? text.IsHtml : text.IsPlain)) { + body = text; + return true; + } + } + + body = null; + + return false; + } + + static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, out BodyPartText body) + { + BodyPartMultipart multi; + BodyPartText text; + + if (multipart.ContentType.IsMimeType ("multipart", "alternative")) + return TryGetMultipartAlternativeBody (multipart, html, out body); + + if (!multipart.ContentType.IsMimeType ("multipart", "related")) { + // Note: This is probably a multipart/mixed... and if not, we can still treat it like it is. + for (int i = 0; i < multipart.BodyParts.Count; i++) { + multi = multipart.BodyParts[i] as BodyPartMultipart; + + // descend into nested multiparts, if there are any... + if (multi != null) { + if (TryGetMessageBody (multi, html, out body)) + return true; + + // The text body should never come after a multipart. + break; + } + + text = multipart.BodyParts[i] as BodyPartText; + + // Look for the first non-attachment text part (realistically, the body text will + // preceed any attachments, but I'm not sure we can rely on that assumption). + if (text != null && !text.IsAttachment) { + if (html ? text.IsHtml : text.IsPlain) { + body = text; + return true; + } + + // Note: the first text/* part in a multipart/mixed is the text body. + // If it's not in the format we're looking for, then it doesn't exist. + break; + } + } + } else { + // Note: If the multipart/related root document is HTML, then this is the droid we are looking for. + var root = GetMultipartRelatedRoot (multipart); + + text = root as BodyPartText; + + if (text != null) { + body = (html ? text.IsHtml : text.IsPlain) ? text : null; + return body != null; + } + + // maybe the root is another multipart (like multipart/alternative)? + multi = root as BodyPartMultipart; + + if (multi != null) + return TryGetMessageBody (multi, html, out body); + } + + body = null; + + return false; + } + + /// + /// Gets the text body part of the message if it exists. + /// + /// + /// Gets the text/plain body part of the message. + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// + /// + /// + /// The text body if it exists; otherwise, null. + public BodyPartText TextBody { + get { + var multipart = Body as BodyPartMultipart; + + if (multipart != null) { + BodyPartText plain; + + if (TryGetMessageBody (multipart, false, out plain)) + return plain; + } else { + var text = Body as BodyPartText; + + if (text != null && text.IsPlain) + return text; + } + + return null; + } + } + + /// + /// Gets the html body part of the message if it exists. + /// + /// + /// Gets the text/html body part of the message. + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The html body if it exists; otherwise, null. + public BodyPartText HtmlBody { + get { + var multipart = Body as BodyPartMultipart; + + if (multipart != null) { + BodyPartText html; + + if (TryGetMessageBody (multipart, true, out html)) + return html; + } else { + var text = Body as BodyPartText; + + if (text != null && text.IsHtml) + return text; + } + + return null; + } + } + + static IEnumerable EnumerateBodyParts (BodyPart entity, bool attachmentsOnly) + { + if (entity == null) + yield break; + + var multipart = entity as BodyPartMultipart; + + if (multipart != null) { + foreach (var subpart in multipart.BodyParts) { + foreach (var part in EnumerateBodyParts (subpart, attachmentsOnly)) + yield return part; + } + + yield break; + } + + var basic = (BodyPartBasic) entity; + + if (attachmentsOnly && !basic.IsAttachment) + yield break; + + yield return basic; + } + + /// + /// Gets the body parts of the message. + /// + /// + /// Traverses over the , enumerating all of the + /// objects. + /// This property will only be usable if either the + /// flag or the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The body parts. + public IEnumerable BodyParts { + get { return EnumerateBodyParts (Body, false); } + } + + /// + /// Gets the attachments. + /// + /// + /// Traverses over the , enumerating all of the + /// objects that have a Content-Disposition + /// header set to "attachment". + /// This property will only be usable if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// + /// + /// + /// The attachments. + public IEnumerable Attachments { + get { return EnumerateBodyParts (Body, true); } + } + + /// + /// Gets the preview text of the message. + /// + /// + /// The preview text is a short snippet of the beginning of the message + /// text, typically shown in a mail client's message list to provide the user + /// with a sense of what the message is about. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The preview text. + public string PreviewText { + get; set; + } + + /// + /// Gets the envelope of the message, if available. + /// + /// + /// The envelope of a message contains information such as the + /// date the message was sent, the subject of the message, + /// the sender of the message, who the message was sent to, + /// which message(s) the message may be in reply to, + /// and the message id. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The envelope of the message. + public Envelope Envelope { + get; set; + } + + /// + /// Gets the normalized subject. + /// + /// + /// A normalized Subject header value where prefixes such as + /// "Re:", "Re[#]:", etc have been pruned. + /// + /// The normalized subject. + public string NormalizedSubject { + get { + UpdateThreadableSubject (); + + return normalizedSubject; + } + } + + /// + /// Gets whether or not the message is a reply. + /// + /// + /// This value should be based on whether the message subject contained any "Re:" or "Fwd:" prefixes. + /// + /// true if the message is a reply; otherwise, false. + public bool IsReply { + get { + UpdateThreadableSubject (); + + return threadableReplyDepth != 0; + } + } + + /// + /// Gets the Date header value. + /// + /// + /// Gets the Date header value. If the Date header is not present, the arrival date is used. + /// If neither are known, is returned. + /// + /// The date. + public DateTimeOffset Date { + get { return Envelope?.Date ?? InternalDate ?? DateTimeOffset.MinValue; } + } + + /// + /// Gets the message flags, if available. + /// + /// + /// Gets the message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message flags. + public MessageFlags? Flags { + get; set; + } + + /// + /// Gets the user-defined message flags, if available. + /// + /// + /// Gets the user-defined message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The user-defined message flags. + public HashSet Keywords { + get; set; + } + + /// + /// Gets the user-defined message flags, if available. + /// + /// + /// Gets the user-defined message flags, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The user-defined message flags. + [Obsolete ("Use Keywords instead.")] + public HashSet UserFlags { + get { return Keywords; } + set { Keywords = value; } + } + + /// + /// Gets the message annotations, if available. + /// + /// + /// Gets the message annotations, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The message annotations. + public IList Annotations { + get; set; + } + + /// + /// Gets the list of headers, if available. + /// + /// + /// Gets the list of headers, if available. + /// This property will only be set if + /// is specified in a call to one of the + /// Fetch + /// or FetchAsync + /// methods or specific headers are requested via a one of the Fetch or FetchAsync methods + /// that accept list of specific headers to request for each message such as + /// . + /// + /// + /// The list of headers. + public HeaderList Headers { + get; set; + } + + /// + /// Gets the internal date of the message (i.e. the "received" date), if available. + /// + /// + /// Gets the internal date of the message (i.e. the "received" date), if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The internal date of the message. + public DateTimeOffset? InternalDate { + get; set; + } + + /// + /// Gets the size of the message, in bytes, if available. + /// + /// + /// Gets the size of the message, in bytes, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The size of the message. + public uint? Size { + get; set; + } + + /// + /// Gets the mod-sequence value for the message, if available. + /// + /// + /// Gets the mod-sequence value for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The mod-sequence value. + public ulong? ModSeq { + get; set; + } + + /// + /// Gets the message-ids that the message references, if available. + /// + /// + /// Gets the message-ids that the message references, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The references. + public MessageIdList References { + get; set; + } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + public string EmailId { + get; set; + } + + /// + /// Get the globally unique identifier for the message, if available. + /// + /// + /// Gets the globally unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the EMAILID value defined in the + /// OBJECTID extension. + /// + /// The globally unique message identifier. + [Obsolete ("Use EmailId instead.")] + public string Id { + get { return EmailId; } + } + + /// + /// Get the globally unique thread identifier for the message, if available. + /// + /// + /// Gets the globally unique thread identifier for the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// This property maps to the THREADID value defined in the + /// OBJECTID extension. + /// + /// The globally unique thread identifier. + public string ThreadId { + get; set; + } + + /// + /// Gets the unique identifier of the message, if available. + /// + /// + /// Gets the unique identifier of the message, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The uid of the message. + public UniqueId UniqueId { + get; set; + } + + /// + /// Gets the index of the message. + /// + /// + /// Gets the index of the message. + /// This property is always set. + /// + /// The index of the message. + public int Index { + get; internal set; + } + + #region GMail extension properties + + /// + /// Gets the GMail message identifier, if available. + /// + /// + /// Gets the GMail message identifier, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail message identifier. + public ulong? GMailMessageId { + get; set; + } + + /// + /// Gets the GMail thread identifier, if available. + /// + /// + /// Gets the GMail thread identifier, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail thread identifier. + public ulong? GMailThreadId { + get; set; + } + + /// + /// Gets the list of GMail labels, if available. + /// + /// + /// Gets the list of GMail labels, if available. + /// This property will only be set if the + /// flag is passed to + /// one of the Fetch + /// or FetchAsync + /// methods. + /// + /// The GMail labels. + public IList GMailLabels { + get; set; + } + + #endregion + } +} diff --git a/src/MailKit/MessageSummaryFetchedEventArgs.cs b/src/MailKit/MessageSummaryFetchedEventArgs.cs new file mode 100644 index 0000000..4598cac --- /dev/null +++ b/src/MailKit/MessageSummaryFetchedEventArgs.cs @@ -0,0 +1,67 @@ +// +// MessageSummaryFetchedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a message summary has been fetched from a folder. + /// + /// + /// Event args used when a message summary has been fetched from a folder. + /// + public class MessageSummaryFetchedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new + /// + /// The message summary. + /// + /// is null. + /// + public MessageSummaryFetchedEventArgs (IMessageSummary message) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + } + + /// + /// Get the message summary. + /// + /// + /// Gets the message summary. + /// + /// The message summary. + public IMessageSummary Message { + get; private set; + } + } +} diff --git a/src/MailKit/MessageSummaryItems.cs b/src/MailKit/MessageSummaryItems.cs new file mode 100644 index 0000000..e331735 --- /dev/null +++ b/src/MailKit/MessageSummaryItems.cs @@ -0,0 +1,222 @@ +// +// FetchFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// A bitfield of fields. + /// + /// + /// are used to specify which properties + /// of should be populated by calls to + /// , + /// , or + /// . + /// + [Flags] + public enum MessageSummaryItems { + /// + /// Don't fetch any summary items. + /// + None = 0, + + /// + /// Fetch the . + /// Fetches all ANNOATION values as defined in + /// rfc5257. + /// + Annotations = 1 << 0, + + /// + /// Fetch the . + /// Fetches the BODY value as defined in + /// rfc3501. + /// Unlike , Body will not populate the + /// parameters nor will it populate the + /// , + /// or properties of each + /// body part. This makes Body far less useful than BodyStructure especially when + /// it is desirable to determine whether or not a body part is an attachment. + /// + Body = 1 << 1, + + /// + /// Fetch the (but with more details than ). + /// Fetches the BODYSTRUCTURE value as defined in + /// rfc3501. + /// Unlike , BodyStructure will also populate the + /// parameters as well as the + /// , + /// and properties of each + /// body part. The Content-Disposition information is especially important when trying to + /// determine whether or not a body part is an attachment, for example. + /// + BodyStructure = 1 << 2, + + /// + /// Fetch the . + /// Fetches the ENVELOPE value as defined in + /// rfc3501. + /// + Envelope = 1 << 3, + + /// + /// Fetch the . + /// Fetches the FLAGS value as defined in + /// rfc3501. + /// + Flags = 1 << 4, + + /// + /// Fetch the . + /// Fetches the INTERNALDATE value as defined in + /// rfc3501. + /// + InternalDate = 1 << 5, + + /// + /// Fetch the . + /// Fetches the RFC822.SIZE value as defined in + /// rfc3501. + /// + Size = 1 << 6, + + /// + /// Fetch the . + /// Fetches the MODSEQ value as defined in + /// rfc4551. + /// + ModSeq = 1 << 7, + + /// + /// Fetch the . + /// + References = 1 << 8, + + /// + /// Fetch the . + /// Fetches the UID value as defined in + /// rfc3501. + /// + UniqueId = 1 << 9, + + /// + /// Fetch the . + /// Fetches the EMAILID value as defined in + /// rfc8474. + /// + EmailId = 1 << 10, + + /// + /// Fetch the . + /// Fetches the EMAILID value as defined in + /// rfc8474. + /// + [Obsolete ("Use EmailId instead.")] + Id = EmailId, + + /// + /// Fetch the . + /// Fetches the THREADID value as defined in + /// rfc8474. + /// + ThreadId = 1 << 11, + + #region GMail extension items + + /// + /// Fetch the . + /// Fetches the X-GM-MSGID value as defined in Google's + /// IMAP extensions + /// documentation. + /// + GMailMessageId = 1 << 12, + + /// + /// Fetch the . + /// Fetches the X-GM-THRID value as defined in Google's + /// IMAP extensions + /// documentation. + /// + GMailThreadId = 1 << 13, + + /// + /// Fetch the . + /// Fetches the X-GM-LABELS value as defined in Google's + /// IMAP extensions + /// documentation. + /// + GMailLabels = 1 << 14, + + #endregion + + /// + /// Fetch the the complete list of for each message. + /// + Headers = 1 << 15, + + /// + /// Fetch the . + /// This property is quite expensive to calculate because it is not an + /// item that is cached on the IMAP server. Instead, MailKit must download a hunk of the + /// message body so that it can decode and parse it in order to generate a meaningful + /// text snippet. This usually involves downloading the first 512 bytes for text/plain + /// message bodies and the first 16 kilobytes for text/html message bodies. If a + /// message contains both a text/plain body and a text/html body, then the + /// text/plain content is used in order to reduce network traffic. + /// + PreviewText = 1 << 16, + + #region Macros + + /// + /// A macro for fetching the , , + /// , and values. + /// This macro maps to the equivalent ALL macro as defined in + /// rfc3501. + /// + All = Envelope | Flags | InternalDate | Size, + + /// + /// A macro for fetching the , , and + /// values. + /// This macro maps to the equivalent FAST macro as defined in + /// rfc3501. + /// + Fast = Flags | InternalDate | Size, + + /// + /// A macro for fetching the , , + /// , , and values. + /// This macro maps to the equivalent FULL macro as defined in + /// rfc3501. + /// + Full = Body | Envelope | Flags| InternalDate | Size, + + #endregion + } +} diff --git a/src/MailKit/MessageThread.cs b/src/MailKit/MessageThread.cs new file mode 100644 index 0000000..c406239 --- /dev/null +++ b/src/MailKit/MessageThread.cs @@ -0,0 +1,108 @@ +// +// MessageThread.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit { + /// + /// A message thread. + /// + /// + /// A message thread. + /// + public class MessageThread + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new message thread node. + /// + /// The unique identifier of the message. + public MessageThread (UniqueId? uid) + { + Children = new List (); + UniqueId = uid; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new message thread node. + /// + /// The message summary. + public MessageThread (IMessageSummary message) + { + Children = new List (); + if (message != null && message.UniqueId.IsValid) + UniqueId = message?.UniqueId; + Message = message; + } + + /// + /// Gets the message summary, if available. + /// + /// + /// Gets the message summary, if available. + /// This property will only ever be set if the + /// was created by the . s that are + /// created by any of the + /// Thread or + /// ThreadAsync + /// methods will always be null. + /// + /// The message summary. + public IMessageSummary Message { + get; private set; + } + + /// + /// Gets the unique identifier of the message. + /// + /// + /// The unique identifier may be null if the message is missing from the + /// or from the list of messages provided to the + /// . + /// + /// The unique identifier. + public UniqueId? UniqueId { + // FIXME: this shouldn't be a nullable since we can just use UniqueId.Invalid + get; private set; + } + + /// + /// Gets the children. + /// + /// + /// Each child represents a reply to the message referenced by . + /// + /// The children. + public IList Children { + get; private set; + } + } +} diff --git a/src/MailKit/MessageThreader.cs b/src/MailKit/MessageThreader.cs new file mode 100644 index 0000000..1f965d5 --- /dev/null +++ b/src/MailKit/MessageThreader.cs @@ -0,0 +1,603 @@ +// +// MessageThreader.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.Utils; + +using MailKit.Search; + +namespace MailKit { + /// + /// Threads messages according to the algorithms defined in rfc5256. + /// + /// + /// Threads messages according to the algorithms defined in rfc5256. + /// + public static class MessageThreader + { + internal class ThreadableNode : IMessageSummary + { + public readonly List Children = new List (); + public IMessageSummary Message; + public ThreadableNode Parent; + + public ThreadableNode (IMessageSummary message) + { + Message = message; + } + + public bool HasParent { + get { return Parent != null; } + } + + public bool HasChildren { + get { return Children.Count > 0; } + } + + public IMailFolder Folder => null; + + public MessageSummaryItems Fields { + get { return MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.ModSeq | MessageSummaryItems.Size; } + } + + public BodyPart Body => null; + + public BodyPartText TextBody => null; + + public BodyPartText HtmlBody => null; + + public IEnumerable BodyParts => null; + + public IEnumerable Attachments => null; + + public string PreviewText => null; + + public Envelope Envelope { + get { return Message != null ? Message.Envelope : Children[0].Envelope; } + } + + public string NormalizedSubject { + get { return Message != null ? Message.NormalizedSubject : Children[0].NormalizedSubject; } + } + + public DateTimeOffset Date { + get { return Message != null ? Message.Date : Children[0].Date; } + } + + public bool IsReply { + get { return Message != null && Message.IsReply; } + } + + public MessageFlags? Flags => null; + + public HashSet Keywords => null; + + [Obsolete] + public HashSet UserFlags => null; + + public IList Annotations { + get { return Message != null ? Message.Annotations : Children[0].Annotations; } + } + + public HeaderList Headers => null; + + public DateTimeOffset? InternalDate => null; + + public uint? Size { + get { return Message != null ? Message.Size : Children[0].Size; } + } + + public ulong? ModSeq { + get { return Message != null ? Message.ModSeq : Children[0].ModSeq; } + } + + public MessageIdList References { + get { return Message != null ? Message.References : Children[0].References; } + } + + public string EmailId => null; + + [Obsolete] + public string Id => null; + + public string ThreadId => null; + + public UniqueId UniqueId { + get { return Message != null ? Message.UniqueId : Children[0].UniqueId; } + } + + public int Index { + get { return Message != null ? Message.Index : Children[0].Index; } + } + + public ulong? GMailMessageId => null; + + public ulong? GMailThreadId => null; + + public IList GMailLabels => null; + } + + static IDictionary CreateIdTable (IEnumerable messages) + { + var ids = new Dictionary (StringComparer.OrdinalIgnoreCase); + ThreadableNode node; + + foreach (var message in messages) { + if (message.Envelope == null) + throw new ArgumentException ("One or more messages is missing information needed for threading.", nameof (messages)); + + var id = message.Envelope.MessageId; + + if (string.IsNullOrEmpty (id)) + id = MimeUtils.GenerateMessageId (); + + if (ids.TryGetValue (id, out node)) { + if (node.Message == null) { + // a previously processed message referenced this message + node.Message = message; + } else { + // a duplicate message-id, just create a dummy id and use that + id = MimeUtils.GenerateMessageId (); + node = null; + } + } + + if (node == null) { + // create a new ThreadContainer for this message and add it to ids + node = new ThreadableNode (message); + ids.Add (id, node); + } + + ThreadableNode parent = null; + foreach (var reference in message.References) { + ThreadableNode referenced; + + if (!ids.TryGetValue (reference, out referenced)) { + // create a dummy container for the referenced message + referenced = new ThreadableNode (null); + ids.Add (reference, referenced); + } + + // chain up the references, disallowing loops + if (parent != null && referenced.Parent == null && parent != referenced && !parent.Children.Contains (referenced)) { + parent.Children.Add (referenced); + referenced.Parent = parent; + } + + parent = referenced; + } + + // don't allow loops + if (parent != null && (parent == node || node.Children.Contains (parent))) + parent = null; + + if (node.HasParent) { + // unlink from our old parent + node.Parent.Children.Remove (node); + node.Parent = null; + } + + if (parent != null) { + // add it as a child of our new parent + parent.Children.Add (node); + node.Parent = parent; + } + } + + return ids; + } + + static ThreadableNode CreateRoot (IDictionary ids) + { + var root = new ThreadableNode (null); + + foreach (var message in ids.Values) { + if (message.Parent == null) + root.Children.Add (message); + } + + return root; + } + + static void PruneEmptyContainers (ThreadableNode root) + { + for (int i = 0; i < root.Children.Count; i++) { + var node = root.Children[i]; + + if (node.Message == null && node.Children.Count == 0) { + // this is an empty container with no children, nuke it. + root.Children.RemoveAt (i); + i--; + } else if (node.Message == null && node.HasChildren && (node.HasParent || node.Children.Count == 1)) { + // If the Container has no Message, but does have children, remove this container but promote + // its children to this level (that is, splice them in to the current child list.) + // + // Do not promote the children if doing so would promote them to the root set -- unless there + // is only one child, in which case, do. + root.Children.RemoveAt (i); + + for (int j = 0; j < node.Children.Count; j++) { + node.Children[j].Parent = node.Parent; + root.Children.Add (node.Children[j]); + } + + node.Children.Clear (); + i--; + } else if (node.HasChildren) { + PruneEmptyContainers (node); + } + } + } + + static void GroupBySubject (ThreadableNode root) + { + var subjects = new Dictionary (StringComparer.OrdinalIgnoreCase); + ThreadableNode match; + int count = 0; + + for (int i = 0; i < root.Children.Count; i++) { + var current = root.Children[i]; + var subject = current.NormalizedSubject; + + // don't thread messages with empty subjects + if (string.IsNullOrEmpty (subject)) + continue; + + if (!subjects.TryGetValue (subject, out match) || + (current.Message == null && match.Message != null) || + (match.Message != null && match.Message.IsReply && + current.Message != null && !current.Message.IsReply)) { + subjects[subject] = current; + count++; + } + } + + if (count == 0) + return; + + for (int i = 0; i < root.Children.Count; i++) { + var current = root.Children[i]; + var subject = current.NormalizedSubject; + + // don't thread messages with empty subjects + if (string.IsNullOrEmpty (subject)) + continue; + + match = subjects[subject]; + + if (match == current) + continue; + + // remove the second message with the same subject + root.Children.RemoveAt (i--); + + // group these messages together... + if (match.Message == null && current.Message == null) { + // If both messages are dummies, append the current message's children + // to the children of the message in the subject table (the children of + // both messages become siblings), and then delete the current message. + match.Children.AddRange (current.Children); + } else if (match.Message == null && current.Message != null) { + // If the message in the subject table is a dummy and the current message + // is not, make the current message a child of the message in the subject + // table (a sibling of its children). + match.Children.Add (current); + } else if (current.Message.IsReply && !match.Message.IsReply) { + // If the current message is a reply or forward and the message in the + // subject table is not, make the current message a child of the message + // in the subject table (a sibling of its children). + match.Children.Add (current); + } else { + // Otherwise, create a new dummy message and make both the current message + // and the message in the subject table children of the dummy. Then replace + // the message in the subject table with the dummy message. + + // Note: if we re-use the node already in the subject table and the root, then + // we won't have to insert the new dummy node at the matched node's location + var dummy = match; + + // clone the message already in the subject table + match = new ThreadableNode (dummy.Message); + match.Children.AddRange (dummy.Children); + + // empty out the old match node (aka the new dummy node) + dummy.Children.Clear (); + dummy.Message = null; + + // now add both messages to the dummy + dummy.Children.Add (match); + dummy.Children.Add (current); + } + } + } + + static void GetThreads (ThreadableNode root, IList threads, IList orderBy) + { + root.Children.Sort (orderBy); + + for (int i = 0; i < root.Children.Count; i++) { + var message = root.Children[i].Message; + var thread = new MessageThread (message); + + GetThreads (root.Children[i], thread.Children, orderBy); + threads.Add (thread); + } + } + + static IList ThreadByReferences (IEnumerable messages, IList orderBy) + { + var threads = new List (); + var ids = CreateIdTable (messages); + var root = CreateRoot (ids); + + PruneEmptyContainers (root); + GroupBySubject (root); + + GetThreads (root, threads, orderBy); + + return threads; + } + + static IList ThreadBySubject (IEnumerable messages, IList orderBy) + { + var threads = new List (); + var root = new ThreadableNode (null); + + foreach (var message in messages) { + if (message.Envelope == null) + throw new ArgumentException ("One or more messages is missing information needed for threading.", nameof (messages)); + + var node = new ThreadableNode (message); + + root.Children.Add (node); + } + + GroupBySubject (root); + + GetThreads (root, threads, orderBy); + + return threads; + } + + /// + /// Thread the messages according to the specified threading algorithm. + /// + /// + /// Thread the messages according to the specified threading algorithm. + /// + /// The threaded messages. + /// The messages. + /// The threading algorithm. + /// + /// is null. + /// + /// + /// is not a valid threading algorithm. + /// + /// + /// contains one or more items that is missing information needed for threading. + /// + public static IList Thread (this IEnumerable messages, ThreadingAlgorithm algorithm) + { + return Thread (messages, algorithm, new [] { OrderBy.Arrival }); + } + + /// + /// Threads the messages according to the specified threading algorithm + /// and sorts the resulting threads by the specified ordering. + /// + /// + /// Threads the messages according to the specified threading algorithm + /// and sorts the resulting threads by the specified ordering. + /// + /// The threaded messages. + /// The messages. + /// The threading algorithm. + /// The requested sort ordering. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid threading algorithm. + /// + /// + /// contains one or more items that is missing information needed for threading or sorting. + /// -or- + /// is an empty list. + /// + public static IList Thread (this IEnumerable messages, ThreadingAlgorithm algorithm, IList orderBy) + { + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + switch (algorithm) { + case ThreadingAlgorithm.OrderedSubject: return ThreadBySubject (messages, orderBy); + case ThreadingAlgorithm.References: return ThreadByReferences (messages, orderBy); + default: throw new ArgumentOutOfRangeException (nameof (algorithm)); + } + } + + static bool IsForward (string subject, int index) + { + return (subject[index] == 'F' || subject[index] == 'f') && + (subject[index + 1] == 'W' || subject[index + 1] == 'w') && + (subject[index + 2] == 'D' || subject[index + 2] == 'd') && + subject[index + 3] == ':'; + } + + static bool IsReply (string subject, int index) + { + return (subject[index] == 'R' || subject[index] == 'r') && + (subject[index + 1] == 'E' || subject[index + 1] == 'e'); + } + + static void SkipWhiteSpace (string subject, ref int index) + { + while (index < subject.Length && char.IsWhiteSpace (subject[index])) + index++; + } + + static bool IsMailingListName (char c) + { + return c == '-' || c == '_' || char.IsLetterOrDigit (c); + } + + static void SkipMailingListName (string subject, ref int index) + { + while (index < subject.Length && IsMailingListName (subject[index])) + index++; + } + + static bool SkipDigits (string subject, ref int index, out int value) + { + int startIndex = index; + + value = 0; + + while (index < subject.Length && char.IsDigit (subject[index])) { + value = (value * 10) + (subject[index] - '0'); + index++; + } + + return index > startIndex; + } + + /// + /// Gets the threadable subject. + /// + /// + /// Gets the threadable subject. + /// + /// The threadable subject. + /// The Subject header value. + /// The reply depth. + /// + /// is null. + /// + public static string GetThreadableSubject (string subject, out int replyDepth) + { + if (subject == null) + throw new ArgumentNullException (nameof (subject)); + + replyDepth = 0; + + int endIndex = subject.Length; + int startIndex = 0; + int index, count; + int left; + + do { + SkipWhiteSpace (subject, ref startIndex); + index = startIndex; + + if ((left = (endIndex - index)) < 3) + break; + + if (left >= 4 && IsForward (subject, index)) { + // skip over the "Fwd:" prefix + startIndex = index + 4; + replyDepth++; + continue; + } + + if (IsReply (subject, index)) { + if (subject[index + 2] == ':') { + // skip over the "Re:" prefix + startIndex = index + 3; + replyDepth++; + continue; + } + + if (subject[index + 2] == '[' || subject[index + 2] == '(') { + char close = subject[index + 2] == '[' ? ']' : ')'; + + // skip over "Re[" or "Re(" + index += 3; + + // if this is followed by "###]:" or "###):", then it's a condensed "Re:" + if (SkipDigits (subject, ref index, out count) && (endIndex - index) >= 2 && + subject[index] == close && subject[index + 1] == ':') { + startIndex = index + 2; + replyDepth += count; + continue; + } + } + } else if (subject[index] == '[' && char.IsLetterOrDigit (subject[index + 1])) { + // possibly a mailing-list prefix + index += 2; + + SkipMailingListName (subject, ref index); + + if ((endIndex - index) >= 1 && subject[index] == ']') { + startIndex = index + 1; + continue; + } + } + + break; + } while (true); + + // trim trailing whitespace + while (endIndex > 0 && char.IsWhiteSpace (subject[endIndex - 1])) + endIndex--; + + // canonicalize the remainder of the subject, condensing multiple spaces into 1 + var builder = new StringBuilder (); + bool lwsp = false; + + for (int i = startIndex; i < endIndex; i++) { + if (char.IsWhiteSpace (subject[i])) { + if (!lwsp) { + builder.Append (' '); + lwsp = true; + } + } else { + builder.Append (subject[i]); + lwsp = false; + } + } + + var canonicalized = builder.ToString (); + + if (canonicalized.Equals ("(no subject)", StringComparison.OrdinalIgnoreCase)) + canonicalized = string.Empty; + + return canonicalized; + } + } +} diff --git a/src/MailKit/MessagesVanishedEventArgs.cs b/src/MailKit/MessagesVanishedEventArgs.cs new file mode 100644 index 0000000..61b8326 --- /dev/null +++ b/src/MailKit/MessagesVanishedEventArgs.cs @@ -0,0 +1,79 @@ +// +// MessagesVanishedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MailKit { + /// + /// Event args used when a message vanishes from a folder. + /// + /// + /// Event args used when a message vanishes from a folder. + /// + public class MessagesVanishedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The list of unique identifiers. + /// If set to true, the messages vanished in the past as opposed to just now. + /// + /// is null. + /// + public MessagesVanishedEventArgs (IList uids, bool earlier) + { + UniqueIds = new ReadOnlyCollection (uids); + Earlier = earlier; + } + + /// + /// Gets the unique identifiers of the messages that vanished. + /// + /// + /// Gets the unique identifiers of the messages that vanished. + /// + /// The unique identifiers. + public IList UniqueIds { + get; private set; + } + + /// + /// Gets whether the messages vanished in the past as opposed to just now. + /// + /// + /// Gets whether the messages vanished in the past as opposed to just now. + /// + /// true if the messages vanished earlier; otherwise, false. + public bool Earlier { + get; private set; + } + } +} diff --git a/src/MailKit/Metadata.cs b/src/MailKit/Metadata.cs new file mode 100644 index 0000000..03d24e6 --- /dev/null +++ b/src/MailKit/Metadata.cs @@ -0,0 +1,74 @@ +// +// Metadata.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// A metadata tag and value. + /// + /// + /// A metadata tag and value. + /// + public class Metadata + { + internal string EncodedName; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The metadata tag. + /// The metadata value. + public Metadata (MetadataTag tag, string value) + { + Value = value; + Tag = tag; + } + + /// + /// Gets the metadata tag. + /// + /// + /// Gets the metadata tag. + /// + /// The metadata tag. + public MetadataTag Tag { + get; private set; + } + + /// + /// Gets the metadata value. + /// + /// + /// Gets the metadata value. + /// + /// The metadata value. + public string Value { + get; private set; + } + } +} diff --git a/src/MailKit/MetadataChangedEventArgs.cs b/src/MailKit/MetadataChangedEventArgs.cs new file mode 100644 index 0000000..f07ae0e --- /dev/null +++ b/src/MailKit/MetadataChangedEventArgs.cs @@ -0,0 +1,67 @@ +// +// MetadataChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Event args used when a metadata changes. + /// + /// + /// Event args used when a metadata changes. + /// + public class MetadataChangedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The metadata that changed. + /// + /// is null. + /// + public MetadataChangedEventArgs (Metadata metadata) + { + if (metadata == null) + throw new ArgumentNullException (nameof (metadata)); + + Metadata = metadata; + } + + /// + /// Get the metadata that changed. + /// + /// + /// Gets the metadata that changed. + /// + /// The metadata. + public Metadata Metadata { + get; private set; + } + } +} diff --git a/src/MailKit/MetadataCollection.cs b/src/MailKit/MetadataCollection.cs new file mode 100644 index 0000000..20c7651 --- /dev/null +++ b/src/MailKit/MetadataCollection.cs @@ -0,0 +1,59 @@ +// +// MetadataCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit { + /// + /// A collection of metadata. + /// + /// + /// A collection of metadata. + /// + public class MetadataCollection : List + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new + /// + public MetadataCollection () + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// A collection of metadata. + public MetadataCollection (IEnumerable collection) : base (collection) + { + } + } +} diff --git a/src/MailKit/MetadataOptions.cs b/src/MailKit/MetadataOptions.cs new file mode 100644 index 0000000..77735bf --- /dev/null +++ b/src/MailKit/MetadataOptions.cs @@ -0,0 +1,108 @@ +// +// MetadataOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit +{ + /// + /// A set of options to use when requesting metadata. + /// + /// + /// A set of options to use when requesting metadata. + /// + public class MetadataOptions + { + int depth; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new set of options to use when requesting metadata. + /// + public MetadataOptions () + { + } + + /// + /// Get or set the depth. + /// + /// + /// When the option is specified, it extends the list of metadata tag + /// values returned by the GetMetadata() call. For each specified in the + /// the GetMetadata() call, the method returns the value of the specified metadata tag (if it exists), + /// plus all metadata tags below the specified entry up to the specified depth. + /// Three values are allowed for : + /// 0 - no entries below the specified metadata tag are returned. + /// 1 - only entries immediately below the specified metadata tag are returned. + /// - all entries below the specified metadata tag are returned. + /// Thus, a depth of 1 for a tag entry of "/a" will match "/a" as well as its children + /// entries (e.g., "/a/b"), but will not match grandchildren entries (e.g., "/a/b/c"). + /// If the Depth option is not specified, this is the same as specifying 0. + /// + /// The depth. + /// + /// is out of range. + /// + public int Depth { + get { return depth; } + set { + if (!(value == 0 || value == 1 || value == int.MaxValue)) + throw new ArgumentOutOfRangeException (nameof (value)); + + depth = value; + } + } + + /// + /// Get or set the max size of the metadata tags to request. + /// + /// + /// When specified, the property is used to filter the metadata tags + /// returned by the GetMetadata() call to only those with a value shorter than the max size + /// specified. + /// + /// The size of the max. + public uint? MaxSize { + get; set; + } + + /// + /// Get the length of the longest metadata value. + /// + /// + /// If the property is specified, once the GetMetadata() call returns, + /// the property will be set to the length of the longest metadata + /// value that exceeded the limit, otherwise a value of 0 will + /// be set. + /// + /// The length of the longest metadata value that exceeded the max size. + public uint LongEntries { + get; set; + } + } +} diff --git a/src/MailKit/MetadataTag.cs b/src/MailKit/MetadataTag.cs new file mode 100644 index 0000000..624da0d --- /dev/null +++ b/src/MailKit/MetadataTag.cs @@ -0,0 +1,156 @@ +// +// MetadataTag.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// A metadata tag. + /// + /// + /// A metadata tag. + /// + public struct MetadataTag + { + /// + /// A metadata tag for specifying the contact information for the server administrator. + /// + /// + /// Used to get the contact information of the administrator on a + /// . + /// + public static readonly MetadataTag SharedAdmin = new MetadataTag ("/shared/admin"); + + /// + /// A metadata tag for private comments. + /// + /// + /// Used to get or set a private comment on a . + /// + public static readonly MetadataTag PrivateComment = new MetadataTag ("/private/comment"); + + /// + /// A metadata tag for shared comments. + /// + /// + /// Used to get or set a shared comment on a + /// or . + /// + public static readonly MetadataTag SharedComment = new MetadataTag ("/shared/comment"); + + /// + /// A metadata tag for specifying the special use of a folder. + /// + /// + /// Used to get or set the special use of a . + /// + public static readonly MetadataTag PrivateSpecialUse = new MetadataTag ("/private/specialuse"); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new . + /// + /// The metadata tag identifier. + /// + /// is null. + /// + /// + /// is an empty string. + /// + public MetadataTag (string id) + { + if (id == null) + throw new ArgumentNullException (nameof (id)); + + if (id.Length == 0) + throw new ArgumentException ("A metadata tag identifier cannot be empty."); + + Id = id; + } + + /// + /// Get the metadata tag identifier. + /// + /// + /// Gets the metadata tag identifier. + /// + /// The metadata tag identifier. + public string Id { + get; private set; + } + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals (object obj) + { + return obj is MetadataTag && ((MetadataTag) obj).Id == Id; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Id.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Id; + } + + internal static MetadataTag Create (string id) + { + switch (id) { + case "/shared/admin": return SharedAdmin; + case "/private/comment": return PrivateComment; + case "/shared/comment": return SharedComment; + case "/private/specialuse": return PrivateSpecialUse; + default: return new MetadataTag (id); + } + } + } +} diff --git a/src/MailKit/ModSeqChangedEventArgs.cs b/src/MailKit/ModSeqChangedEventArgs.cs new file mode 100644 index 0000000..3aaca32 --- /dev/null +++ b/src/MailKit/ModSeqChangedEventArgs.cs @@ -0,0 +1,86 @@ +// +// ModSeqChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit +{ + /// + /// Event args for the event. + /// + /// + /// Event args for the event. + /// + public class ModSeqChangedEventArgs : MessageEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + internal ModSeqChangedEventArgs (int index) : base (index) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The modification sequence value. + public ModSeqChangedEventArgs (int index, ulong modseq) : base (index) + { + ModSeq = modseq; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message index. + /// The unique id of the message. + /// The modification sequence value. + public ModSeqChangedEventArgs (int index, UniqueId uid, ulong modseq) : base (index, uid) + { + ModSeq = modseq; + } + + /// + /// Gets the updated mod-sequence value of the message. + /// + /// + /// Gets the updated mod-sequence value of the message. + /// + /// The mod-sequence value. + public ulong ModSeq { + get; internal set; + } + } +} diff --git a/src/MailKit/Net/Imap/AsyncImapClient.cs b/src/MailKit/Net/Imap/AsyncImapClient.cs new file mode 100644 index 0000000..b88d25d --- /dev/null +++ b/src/MailKit/Net/Imap/AsyncImapClient.cs @@ -0,0 +1,965 @@ +// +// AsyncImapClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MailKit.Security; + +namespace MailKit.Net.Imap +{ + public partial class ImapClient + { + /// + /// Asynchronously enable compression over the IMAP connection. + /// + /// + /// Asynchronously enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the COMPRESS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task CompressAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return CompressAsync (true, cancellationToken); + } + + /// + /// Asynchronously enable the QRESYNC feature. + /// + /// + /// Enables the QRESYNC feature. + /// The QRESYNC extension improves resynchronization performance of folders by + /// querying the IMAP server for a list of changes when the folder is opened using the + /// + /// method. + /// If this feature is enabled, the event is replaced + /// with the event. + /// This method needs to be called immediately after calling one of the + /// Authenticate methods, before + /// opening any folders. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task EnableQuickResyncAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return EnableQuickResyncAsync (true, cancellationToken); + } + + /// + /// Asynchronously enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)) + { + return EnableUTF8Async (true, cancellationToken); + } + + /// + /// Asynchronously identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is null or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing null as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// The implementation details of the server if available; otherwise, null. + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task IdentifyAsync (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)) + { + return IdentifyAsync (clientImplementation, true, cancellationToken); + } + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (mechanism, true, cancellationToken); + } + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// If the IMAP server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then LOGIN command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (encoding, credentials, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified IMAP server. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 993. All other values will use a default port of 143. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified IMAP or IMAP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (socket, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified IMAP or IMAP/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (stream, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is true, a LOGOUT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to true, a LOGOUT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + return DisconnectAsync (quit, true, cancellationToken); + } + + /// + /// Asynchronously ping the IMAP server to keep the connection alive. + /// + /// + /// The NOOP command is typically used to keep the connection with the IMAP server + /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the + /// IMAP server, the IMAP server will close the connection with the client, forcing the client to + /// reconnect before it can send any more commands. + /// The NOOP command also provides a great way for a client to check for new + /// messages. + /// When the IMAP server receives a NOOP command, it will reply to the client with a + /// list of pending updates such as EXISTS and RECENT counts on the currently + /// selected folder. To receive these notifications, subscribe to the + /// and events, + /// respectively. + /// For more information about the NOOP command, see + /// rfc3501. + /// + /// + /// + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOOP command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return NoOpAsync (true, cancellationToken); + } + + /// + /// Asynchronously toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// An asynchronous task context. + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public Task IdleAsync (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) + { + return IdleAsync (doneToken, true, cancellationToken); + } + + /// + /// Asynchronously request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// An asynchronous task context. + /// true if the server should immediately notify the client of the + /// selected folder's status; otherwise, false. + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public Task NotifyAsync (bool status, IList eventGroups, CancellationToken cancellationToken = default (CancellationToken)) + { + return NotifyAsync (status, eventGroups, true, cancellationToken); + } + + /// + /// Asynchronously disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public Task DisableNotifyAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return DisableNotifyAsync (true, cancellationToken); + } + + /// + /// Asynchronously get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the LIST or LSUB command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetFoldersAsync (@namespace, items, subscribedOnly, true, cancellationToken); + } + + /// + /// Asynchronously get the folder for the specified path. + /// + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override async Task GetFolderAsync (string path, CancellationToken cancellationToken = default (CancellationToken)) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return await engine.GetFolderAsync (path, true, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (tag, true, cancellationToken); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (options, tags, true, cancellationToken); + } + + /// + /// Asynchronously gets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetMetadataAsync (metadata, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Imap/IImapClient.cs b/src/MailKit/Net/Imap/IImapClient.cs new file mode 100644 index 0000000..a9b693d --- /dev/null +++ b/src/MailKit/Net/Imap/IImapClient.cs @@ -0,0 +1,628 @@ +// +// IImapClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Imap { + /// + /// An interface for an IMAP client. + /// + /// + /// Implemented by . + /// + public interface IImapClient : IMailStore + { + /// + /// Get the capabilities supported by the IMAP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made via one of + /// the Connect methods and may + /// change as a side-effect of calling one of the + /// Authenticate + /// methods. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + ImapCapabilities Capabilities { get; set; } + + /// + /// Gets the maximum size of a message that can be appended to a folder. + /// + /// + /// Gets the maximum size of a message, in bytes, that can be appended to a folder. + /// If the value is not set, then the limit is unspecified. + /// + /// The append limit. + uint? AppendLimit { get; } + + /// + /// Gets the internationalization level supported by the IMAP server. + /// + /// + /// Gets the internationalization level supported by the IMAP server. + /// For more information, see + /// section 4 of rfc5255. + /// + /// The internationalization level. + int InternationalizationLevel { get; } + + /// + /// Get the access rights supported by the IMAP server. + /// + /// + /// These rights are additional rights supported by the IMAP server beyond the standard rights + /// defined in section 2.1 of rfc4314 + /// and will not be populated until the client is successfully connected. + /// + /// + /// + /// + /// The rights. + AccessRights Rights { get; } + + /// + /// Get whether or not the client is currently in the IDLE state. + /// + /// + /// Gets whether or not the client is currently in the IDLE state. + /// + /// true if an IDLE command is active; otherwise, false. + bool IsIdle { get; } + + /// + /// Enable compression over the IMAP connection. + /// + /// + /// Enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + void Compress (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously enable compression over the IMAP connection. + /// + /// + /// Asynchronously enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the COMPRESS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task CompressAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is null or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing null as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// + /// + /// + /// The implementation details of the server if available; otherwise, null. + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is null or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing null as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// The implementation details of the server if available; otherwise, null. + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + Task IdentifyAsync (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// + /// + /// + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// An asynchronous task context. + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task IdleAsync (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// true if the server should immediately notify the client of the + /// selected folder's status; otherwise, false. + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void Notify (bool status, IList eventGroups, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// An asynchronous task context. + /// true if the server should immediately notify the client of the + /// selected folder's status; otherwise, false. + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task NotifyAsync (bool status, IList eventGroups, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + void DisableNotify (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + Task DisableNotifyAsync (CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/Net/Imap/IImapFolder.cs b/src/MailKit/Net/Imap/IImapFolder.cs new file mode 100644 index 0000000..2006d6b --- /dev/null +++ b/src/MailKit/Net/Imap/IImapFolder.cs @@ -0,0 +1,869 @@ +// +// IImapFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Search; + +namespace MailKit.Net.Imap { + /// + /// An interface for an IMAP folder. + /// + /// + /// Implemented by . + /// + /// + /// + /// + /// + /// + /// + public interface IImapFolder : IMailFolder + { + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + HeaderList GetHeaders (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetHeadersAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + HeaderList GetHeaders (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetHeadersAsync (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetBodyPartAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + MimeEntity GetBodyPart (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetBodyPartAsync (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The uids of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (IList uids, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The uids of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (IList uids, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The indexes of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (IList indexes, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The indexes of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (IList indexes, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + void GetStreams (int min, int max, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task GetStreamsAsync (int min, int max, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null); + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + SearchResults Search (string query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task SearchAsync (string query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + SearchResults Sort (string query, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + Task SortAsync (string query, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/Net/Imap/ImapCallbacks.cs b/src/MailKit/Net/Imap/ImapCallbacks.cs new file mode 100644 index 0000000..3ba97cb --- /dev/null +++ b/src/MailKit/Net/Imap/ImapCallbacks.cs @@ -0,0 +1,68 @@ +// +// ImapCallbacks.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MailKit.Net.Imap +{ + /// + /// A callback used when fetching message streams. + /// + /// + /// This callback is meant to be used with the various + /// GetStreams + /// methods. + /// Once this callback returns, the stream argument will be disposed, so + /// it is important to consume the stream right away and not add it to a queue + /// for later processing. + /// + /// The IMAP folder that the message belongs to. + /// The index of the message in the folder. + /// The UID of the message in the folder. + /// The raw message (or part) stream. + public delegate void ImapFetchStreamCallback (ImapFolder folder, int index, UniqueId uid, Stream stream); + + /// + /// An asynchronous callback used when fetching message streams. + /// + /// + /// This callback is meant to be used with the various + /// GetStreamsAsync + /// methods. + /// Once this callback returns, the stream argument will be disposed, so + /// it is important to consume the stream right away and not add it to a queue + /// for later processing. + /// + /// An awaitable task context. + /// The IMAP folder that the message belongs to. + /// The index of the message in the folder. + /// The UID of the message in the folder. + /// The raw message (or part) stream. + /// The cancellation token. + public delegate Task ImapFetchStreamAsyncCallback (ImapFolder folder, int index, UniqueId uid, Stream stream, CancellationToken cancellationToken); +} diff --git a/src/MailKit/Net/Imap/ImapCapabilities.cs b/src/MailKit/Net/Imap/ImapCapabilities.cs new file mode 100644 index 0000000..a27b51b --- /dev/null +++ b/src/MailKit/Net/Imap/ImapCapabilities.cs @@ -0,0 +1,348 @@ +// +// ImapCapabilities.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net.Imap { + /// + /// Capabilities supported by an IMAP server. + /// + /// + /// Capabilities are read as part of the response to the CAPABILITY command that + /// is issued during the connection and authentication phases of the + /// . + /// + /// + /// + /// + [Flags] + public enum ImapCapabilities : ulong { + /// + /// The server does not support any additional extensions. + /// + None = 0, + + /// + /// The server implements the core IMAP4 commands. + /// + IMAP4 = 1L << 0, + + /// + /// The server implements the core IMAP4rev1 commands. + /// + IMAP4rev1 = 1L << 1, + + /// + /// The server implements the core IMAP4rev2 commands. + /// + IMAP4rev2 = 1L << 2, + + /// + /// The server supports the STATUS command. + /// + Status = 1L << 3, + + /// + /// The server supports the ACL extension defined in rfc2086 + /// and rfc4314. + /// + Acl = 1L << 4, + + /// + /// The server supports the QUOTA extension. + /// + Quota = 1L << 5, + + /// + /// The server supports the LITERAL+ extension. + /// + LiteralPlus = 1L << 6, + + /// + /// The server supports the IDLE extension. + /// + Idle = 1L << 7, + + /// + /// The server supports the MAILBOX-REFERRALS extension. + /// + MailboxReferrals = 1L << 8, + + /// + /// the server supports the LOGIN-REFERRALS extension. + /// + LoginReferrals = 1L << 9, + + /// + /// The server supports the NAMESPACE extension. + /// + Namespace = 1L << 10, + + /// + /// The server supports the ID extension. + /// + Id = 1L << 11, + + /// + /// The server supports the CHILDREN extension. + /// + Children = 1L << 12, + + /// + /// The server supports the LOGINDISABLED extension. + /// + LoginDisabled = 1L << 13, + + /// + /// The server supports the STARTTLS extension. + /// + StartTLS = 1L << 14, + + /// + /// The server supports the MULTIAPPEND extension. + /// + MultiAppend = 1L << 15, + + /// + /// The server supports the BINARY content extension. + /// + Binary = 1L << 16, + + /// + /// The server supports the UNSELECT extension. + /// + Unselect = 1L << 17, + + /// + /// The server supports the UIDPLUS extension. + /// + UidPlus = 1L << 18, + + /// + /// The server supports the CATENATE extension. + /// + Catenate = 1L << 19, + + /// + /// The server supports the CONDSTORE extension. + /// + CondStore = 1L << 20, + + /// + /// The server supports the ESEARCH extension. + /// + ESearch = 1L << 21, + + /// + /// The server supports the SASL-IR extension. + /// + SaslIR = 1L << 22, + + /// + /// The server supports the COMPRESS extension. + /// + Compress = 1L << 23, + + /// + /// The server supports the WITHIN extension. + /// + Within = 1L << 24, + + /// + /// The server supports the ENABLE extension. + /// + Enable = 1L << 25, + + /// + /// The server supports the QRESYNC extension. + /// + QuickResync = 1L << 26, + + /// + /// The server supports the SEARCHRES extension. + /// + SearchResults = 1L << 27, + + /// + /// The server supports the SORT extension. + /// + Sort = 1L << 28, + + /// + /// The server supports the THREAD extension. + /// + Thread = 1L << 29, + + /// + /// The server supports the ANNOTATE extension. + /// + Annotate = 1L << 30, + + /// + /// The server supports the LIST-EXTENDED extension. + /// + ListExtended = 1L << 31, + + /// + /// The server supports the CONVERT extension. + /// + Convert = 1L << 32, + + /// + /// The server supports the LANGUAGE extension. + /// + Language = 1L << 33, + + /// + /// The server supports the I18NLEVEL extension. + /// + I18NLevel = 1L << 34, + + /// + /// The server supports the ESORT extension. + /// + ESort = 1L << 35, + + /// + /// The server supports the CONTEXT extension. + /// + Context = 1L << 36, + + /// + /// The server supports the METADATA extension. + /// + Metadata = 1L << 37, + + /// + /// The server supports the METADATA-SERVER extension. + /// + MetadataServer = 1L << 38, + + /// + /// The server supports the NOTIFY extension. + /// + Notify = 1L << 39, + + /// + /// The server supports the FILTERS extension. + /// + Filters = 1L << 40, + + /// + /// The server supports the LIST-STATUS extension. + /// + ListStatus = 1L << 41, + + /// + /// The server supports the SORT=DISPLAY extension. + /// + SortDisplay = 1L << 42, + + /// + /// The server supports the CREATE-SPECIAL-USE extension. + /// + CreateSpecialUse = 1L << 43, + + /// + /// The server supports the SPECIAL-USE extension. + /// + SpecialUse = 1L << 44, + + /// + /// The server supports the SEARCH=FUZZY extension. + /// + FuzzySearch = 1L << 45, + + /// + /// The server supports the MULTISEARCH extension. + /// + MultiSearch = 1L << 46, + + /// + /// The server supports the MOVE extension. + /// + Move = 1L << 47, + + /// + /// The server supports the UTF8=ACCEPT extension. + /// + UTF8Accept = 1L << 48, + + /// + /// The server supports the UTF8=ONLY extension. + /// + UTF8Only = 1L << 49, + + /// + /// The server supports the LITERAL- extension. + /// + LiteralMinus = 1L << 50, + + /// + /// The server supports the APPENDLIMIT extension. + /// + AppendLimit = 1L << 51, + + /// + /// The server supports the UNAUTHENTICATE extension. + /// + Unauthenticate = 1L << 52, + + /// + /// The server supports the STATUS=SIZE extension. + /// + StatusSize = 1L << 53, + + /// + /// The server supports the LIST-MYRIGHTS extension. + /// + ListMyRights = 1L << 54, + + /// + /// The server supports the OBJECTID extension. + /// + ObjectID = 1L << 55, + + /// + /// The server supports the REPLACE extension. + /// + Replace = 1L << 56, + + #region GMail Extensions + + /// + /// The server supports the XLIST extension (GMail). + /// + XList = 1L << 60, + + /// + /// The server supports the X-GM-EXT1 extension (GMail). + /// + GMailExt1 = 1L << 61 + + #endregion + } +} diff --git a/src/MailKit/Net/Imap/ImapClient.cs b/src/MailKit/Net/Imap/ImapClient.cs new file mode 100644 index 0000000..acd199c --- /dev/null +++ b/src/MailKit/Net/Imap/ImapClient.cs @@ -0,0 +1,2616 @@ +// +// ImapClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +using MailKit.Security; + +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Imap { + /// + /// An IMAP client that can be used to retrieve messages from a server. + /// + /// + /// The class supports both the "imap" and "imaps" + /// protocols. The "imap" protocol makes a clear-text connection to the IMAP + /// server and does not use SSL or TLS unless the IMAP server supports the + /// STARTTLS extension. + /// The "imaps" protocol, however, connects to the IMAP server using an + /// SSL-wrapped connection. + /// + /// + /// + /// + /// + /// + /// + public partial class ImapClient : MailStore, IImapClient + { + static readonly char[] ReservedUriCharacters = { ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%' }; + const string HexAlphabet = "0123456789ABCDEF"; + + readonly ImapEngine engine; + int timeout = 2 * 60 * 1000; + string identifier; + bool disconnecting; + bool connecting; + bool disposed; + bool secure; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can retrieve messages with the , you must first + /// call one of the Connect + /// methods and then authenticate with the one of the + /// Authenticate + /// methods. + /// + /// + /// + /// + public ImapClient () : this (new NullProtocolLogger ()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can retrieve messages with the , you must first + /// call one of the Connect + /// methods and then authenticate with the one of the + /// Authenticate + /// methods. + /// + /// + /// + /// + /// The protocol logger. + /// + /// is null. + /// + public ImapClient (IProtocolLogger protocolLogger) : base (protocolLogger) + { + // FIXME: should this take a ParserOptions argument? + engine = new ImapEngine (CreateImapFolder); + engine.MetadataChanged += OnEngineMetadataChanged; + engine.FolderCreated += OnEngineFolderCreated; + engine.Disconnected += OnEngineDisconnected; + engine.Alert += OnEngineAlert; + } + + /// + /// Gets an object that can be used to synchronize access to the IMAP server. + /// + /// + /// Gets an object that can be used to synchronize access to the IMAP server. + /// When using the non-Async methods from multiple threads, it is important to lock the + /// object for thread safety when using the synchronous methods. + /// + /// The lock object. + public override object SyncRoot { + get { return engine; } + } + + /// + /// Get the protocol supported by the message service. + /// + /// + /// Gets the protocol supported by the message service. + /// + /// The protocol. + protected override string Protocol { + get { return "imap"; } + } + + /// + /// Get the capabilities supported by the IMAP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made via one of + /// the Connect methods and may + /// change as a side-effect of calling one of the + /// Authenticate + /// methods. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + public ImapCapabilities Capabilities { + get { return engine.Capabilities; } + set { + if ((engine.Capabilities | value) > engine.Capabilities) + throw new ArgumentException ("Capabilities cannot be enabled, they may only be disabled.", nameof (value)); + + engine.Capabilities = value; + } + } + + /// + /// Gets the maximum size of a message that can be appended to a folder. + /// + /// + /// Gets the maximum size of a message, in bytes, that can be appended to a folder. + /// If the value is not set, then the limit is unspecified. + /// + /// The append limit. + public uint? AppendLimit { + get { return engine.AppendLimit; } + } + + /// + /// Gets the internationalization level supported by the IMAP server. + /// + /// + /// Gets the internationalization level supported by the IMAP server. + /// For more information, see + /// section 4 of rfc5255. + /// + /// The internationalization level. + public int InternationalizationLevel { + get { return engine.I18NLevel; } + } + + /// + /// Get the access rights supported by the IMAP server. + /// + /// + /// These rights are additional rights supported by the IMAP server beyond the standard rights + /// defined in section 2.1 of rfc4314 + /// and will not be populated until the client is successfully connected. + /// + /// + /// + /// + /// The rights. + public AccessRights Rights { + get { return engine.Rights; } + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (ImapClient)); + } + + void CheckConnected () + { + if (!IsConnected) + throw new ServiceNotConnectedException ("The ImapClient is not connected."); + } + + void CheckAuthenticated () + { + if (!IsAuthenticated) + throw new ServiceNotAuthenticatedException ("The ImapClient is not authenticated."); + } + + /// + /// Instantiate a new . + /// + /// + /// Creates a new instance. + /// This method's purpose is to allow subclassing . + /// + /// The IMAP folder instance. + /// The constructior arguments. + /// + /// is null. + /// + protected virtual ImapFolder CreateImapFolder (ImapFolderConstructorArgs args) + { + var folder = new ImapFolder (args); + + folder.UpdateAppendLimit (AppendLimit); + + return folder; + } + + bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (ServerCertificateValidationCallback != null) + return ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (ServicePointManager.ServerCertificateValidationCallback != null) + return ServicePointManager.ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); +#endif + + return DefaultServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + } + + async Task CompressAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + + if ((engine.Capabilities & ImapCapabilities.Compress) == 0) + throw new NotSupportedException ("The IMAP server does not support the COMPRESS extension."); + + if (engine.State >= ImapEngineState.Selected) + throw new InvalidOperationException ("Compression must be enabled before selecting a folder."); + + int capabilitiesVersion = engine.CapabilitiesVersion; + var ic = engine.QueueCommand (cancellationToken, null, "COMPRESS DEFLATE\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].Type == ImapResponseCodeType.CompressionActive) + return; + } + + throw ImapCommandException.Create ("COMPRESS", ic); + } + + engine.Stream.Stream = new CompressedStream (engine.Stream.Stream); + } + + /// + /// Enable compression over the IMAP connection. + /// + /// + /// Enables compression over the IMAP connection. + /// If the IMAP server supports the extension, + /// it is possible at any point after connecting to enable compression to reduce network + /// bandwidth usage. Ideally, this method should be called before authenticating. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Compression must be enabled before a folder has been selected. + /// + /// + /// The IMAP server does not support the extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the COMPRESS command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public void Compress (CancellationToken cancellationToken = default (CancellationToken)) + { + CompressAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task EnableQuickResyncAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (engine.State != ImapEngineState.Authenticated) + throw new InvalidOperationException ("QRESYNC needs to be enabled immediately after authenticating."); + + if ((engine.Capabilities & ImapCapabilities.QuickResync) == 0) + throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); + + if (engine.QResyncEnabled) + return; + + var ic = engine.QueueCommand (cancellationToken, null, "ENABLE QRESYNC CONDSTORE\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("ENABLE", ic); + } + + /// + /// Enable the QRESYNC feature. + /// + /// + /// Enables the QRESYNC feature. + /// The QRESYNC extension improves resynchronization performance of folders by + /// querying the IMAP server for a list of changes when the folder is opened using the + /// + /// method. + /// If this feature is enabled, the event is replaced + /// with the event. + /// This method needs to be called immediately after calling one of the + /// Authenticate methods, before + /// opening any folders. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Quick resynchronization needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)) + { + EnableQuickResyncAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task EnableUTF8Async (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (engine.State != ImapEngineState.Authenticated) + throw new InvalidOperationException ("UTF8=ACCEPT needs to be enabled immediately after authenticating."); + + if ((engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8=ACCEPT extension."); + + if (engine.UTF8Enabled) + return; + + var ic = engine.QueueCommand (cancellationToken, null, "ENABLE UTF8=ACCEPT\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("ENABLE", ic); + } + + /// + /// Enable the UTF8=ACCEPT extension. + /// + /// + /// Enables the UTF8=ACCEPT extension. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// UTF8=ACCEPT needs to be enabled before selecting a folder. + /// + /// + /// The IMAP server does not support the UTF8=ACCEPT extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ENABLE command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)) + { + EnableUTF8Async (false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task IdentifyAsync (ImapImplementation clientImplementation, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + + if ((engine.Capabilities & ImapCapabilities.Id) == 0) + throw new NotSupportedException ("The IMAP server does not support the ID extension."); + + var command = new StringBuilder ("ID "); + var args = new List (); + + if (clientImplementation != null && clientImplementation.Properties.Count > 0) { + command.Append ('('); + foreach (var property in clientImplementation.Properties) { + command.Append ("%Q "); + args.Add (property.Key); + + if (property.Value != null) { + command.Append ("%Q "); + args.Add (property.Value); + } else { + command.Append ("NIL "); + } + } + command[command.Length - 1] = ')'; + command.Append ("\r\n"); + } else { + command.Append ("NIL\r\n"); + } + + var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic.RegisterUntaggedHandler ("ID", ImapUtils.ParseImplementationAsync); + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("ID", ic); + + return (ImapImplementation) ic.UserData; + } + + /// + /// Identify the client implementation to the server and obtain the server implementation details. + /// + /// + /// Passes along the client implementation details to the server while also obtaining implementation + /// details from the server. + /// If the is null or no properties have been set, no + /// identifying information will be sent to the server. + /// + /// Security Implications + /// This command has the danger of violating the privacy of users if misused. Clients should + /// notify users that they send the ID command. + /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by + /// not calling this method at all, or by passing null as the + /// argument. + /// Implementors must exercise extreme care in adding properties to the . + /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier + /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for + /// attackers to exploit security holes in the client. + /// + /// + /// + /// + /// + /// The implementation details of the server if available; otherwise, null. + /// The client implementation. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The IMAP server does not support the ID extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the ID command with a NO or BAD response. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)) + { + return IdentifyAsync (clientImplementation, false, cancellationToken).GetAwaiter ().GetResult (); + } + + #region IMailService implementation + + /// + /// Get the authentication mechanisms supported by the IMAP server. + /// + /// + /// The authentication mechanisms are queried as part of the + /// Connect + /// method. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before authenticating. + /// + /// + /// + /// + /// The authentication mechanisms. + public override HashSet AuthenticationMechanisms { + get { return engine.AuthenticationMechanisms; } + } + + /// + /// Get the threading algorithms supported by the IMAP server. + /// + /// + /// The threading algorithms are queried as part of the + /// Connect + /// and Authenticate methods. + /// + /// + /// + /// + /// The supported threading algorithms. + public override HashSet ThreadingAlgorithms { + get { return engine.ThreadingAlgorithms; } + } + + /// + /// Get or set the timeout for network streaming operations, in milliseconds. + /// + /// + /// Gets or sets the underlying socket stream's + /// and values. + /// + /// The timeout in milliseconds. + public override int Timeout { + get { return timeout; } + set { + if (IsConnected && engine.Stream.CanTimeout) { + engine.Stream.WriteTimeout = value; + engine.Stream.ReadTimeout = value; + } + + timeout = value; + } + } + + /// + /// Get whether or not the client is currently connected to an IMAP server. + /// + /// + /// The state is set to true immediately after + /// one of the Connect + /// methods succeeds and is not set back to false until either the client + /// is disconnected via or until an + /// is thrown while attempting to read or write to + /// the underlying network socket. + /// When an is caught, the connection state of the + /// should be checked before continuing. + /// + /// true if the client is connected; otherwise, false. + public override bool IsConnected { + get { return engine.IsConnected; } + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// true if the connection is secure; otherwise, false. + public override bool IsSecure { + get { return IsConnected && secure; } + } + + /// + /// Get whether or not the client is currently authenticated with the IMAP server. + /// + /// + /// Gets whether or not the client is currently authenticated with the IMAP server. + /// To authenticate with the IMAP server, use one of the + /// Authenticate + /// methods. + /// + /// true if the client is connected; otherwise, false. + public override bool IsAuthenticated { + get { return engine.State >= ImapEngineState.Authenticated; } + } + + /// + /// Get whether or not the client is currently in the IDLE state. + /// + /// + /// Gets whether or not the client is currently in the IDLE state. + /// + /// true if an IDLE command is active; otherwise, false. + public bool IsIdle { + get { return engine.State == ImapEngineState.Idle; } + } + + static AuthenticationException CreateAuthenticationException (ImapCommand ic) + { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].IsError || ic.RespCodes[i].Type == ImapResponseCodeType.Alert) + return new AuthenticationException (ic.RespCodes[i].Message); + } + + if (ic.ResponseText != null) + return new AuthenticationException (ic.ResponseText); + + return new AuthenticationException (); + } + + void EmitAndThrowOnAlert (ImapCommand ic) + { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].Type != ImapResponseCodeType.Alert) + continue; + + OnAlert (ic.RespCodes[i].Message); + + throw new AuthenticationException (ic.ResponseText ?? ic.RespCodes[i].Message); + } + } + + static bool IsHexDigit (char c) + { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + static uint HexUnescape (uint c) + { + if (c >= 'a') + return (c - 'a') + 10; + + if (c >= 'A') + return (c - 'A') + 10; + + return c - '0'; + } + + static char HexUnescape (string pattern, ref int index) + { + uint value, c; + + if (pattern[index++] != '%' || !IsHexDigit (pattern[index]) || !IsHexDigit (pattern[index + 1])) + return '%'; + + c = (uint) pattern[index++]; + value = HexUnescape (c) << 4; + c = pattern[index++]; + value |= HexUnescape (c); + + return (char) value; + } + + internal static string UnescapeUserName (string escaped) + { + StringBuilder userName; + int startIndex, index; + + if ((index = escaped.IndexOf ('%')) == -1) + return escaped; + + userName = new StringBuilder (); + startIndex = 0; + + do { + userName.Append (escaped, startIndex, index - startIndex); + userName.Append (HexUnescape (escaped, ref index)); + startIndex = index; + + if (startIndex >= escaped.Length) + break; + + index = escaped.IndexOf ('%', startIndex); + } while (index != -1); + + if (index == -1) + userName.Append (escaped, startIndex, escaped.Length - startIndex); + + return userName.ToString (); + } + + static string HexEscape (char c) + { + return "%" + HexAlphabet[(c >> 4) & 0xF] + HexAlphabet[c & 0xF]; + } + + internal static string EscapeUserName (string userName) + { + StringBuilder escaped; + int startIndex, index; + + if ((index = userName.IndexOfAny (ReservedUriCharacters)) == -1) + return userName; + + escaped = new StringBuilder (); + startIndex = 0; + + do { + escaped.Append (userName, startIndex, index - startIndex); + escaped.Append (HexEscape (userName[index++])); + startIndex = index; + + if (startIndex >= userName.Length) + break; + + index = userName.IndexOfAny (ReservedUriCharacters, startIndex); + } while (index != -1); + + if (index == -1) + escaped.Append (userName, startIndex, userName.Length - startIndex); + + return escaped.ToString (); + } + + string GetSessionIdentifier (string userName) + { + var uri = engine.Uri; + + return string.Format (CultureInfo.InvariantCulture, "{0}://{1}@{2}:{3}", uri.Scheme, EscapeUserName (userName), uri.Host, uri.Port); + } + + async Task OnAuthenticatedAsync (string message, bool doAsync, CancellationToken cancellationToken) + { + await engine.QueryNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false); + await engine.QuerySpecialFoldersAsync (doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (message); + } + + async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, CancellationToken cancellationToken) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + CheckDisposed (); + CheckConnected (); + + if (engine.State >= ImapEngineState.Authenticated) + throw new InvalidOperationException ("The ImapClient is already authenticated."); + + int capabilitiesVersion = engine.CapabilitiesVersion; + var uri = new Uri ("imap://" + engine.Uri.Host); + NetworkCredential cred; + ImapCommand ic = null; + string id; + + cancellationToken.ThrowIfCancellationRequested (); + + mechanism.Uri = uri; + + var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { + var ir = mechanism.Challenge (null); + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge; + + if (mechanism.IsAuthenticated) { + // The server claims we aren't done authenticating, but our SASL mechanism thinks we are... + // Send an empty string to abort the AUTHENTICATE command. + challenge = string.Empty; + } else { + challenge = mechanism.Challenge (text); + } + + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + if (xdoAsync) { + await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + } else { + imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); + } + }; + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) { + EmitAndThrowOnAlert (ic); + + throw new AuthenticationException (); + } + + engine.State = ImapEngineState.Authenticated; + + cred = mechanism.Credentials.GetCredential (mechanism.Uri, mechanism.MechanismName); + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (mechanism, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool doAsync, CancellationToken cancellationToken) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + CheckDisposed (); + CheckConnected (); + + if (engine.State >= ImapEngineState.Authenticated) + throw new InvalidOperationException ("The ImapClient is already authenticated."); + + int capabilitiesVersion = engine.CapabilitiesVersion; + var uri = new Uri ("imap://" + engine.Uri.Host); + NetworkCredential cred; + ImapCommand ic = null; + SaslMechanism sasl; + string id; + + foreach (var authmech in SaslMechanism.AuthMechanismRank) { + if (!engine.AuthenticationMechanisms.Contains (authmech)) + continue; + + if ((sasl = SaslMechanism.Create (authmech, uri, encoding, credentials)) == null) + continue; + + cancellationToken.ThrowIfCancellationRequested (); + + var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { + var ir = sasl.Challenge (null); + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge; + + if (sasl.IsAuthenticated) { + // The server claims we aren't done authenticating, but our SASL mechanism thinks we are... + // Send an empty string to abort the AUTHENTICATE command. + challenge = string.Empty; + } else { + challenge = sasl.Challenge (text); + } + + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + if (xdoAsync) { + await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + } else { + imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); + } + }; + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) { + EmitAndThrowOnAlert (ic); + if (ic.Bye) + throw new ImapProtocolException (ic.ResponseText); + continue; + } + + engine.State = ImapEngineState.Authenticated; + + cred = credentials.GetCredential (uri, sasl.MechanismName); + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + return; + } + + if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { + if (ic == null) + throw new AuthenticationException ("The LOGIN command is disabled."); + + throw CreateAuthenticationException (ic); + } + + // fall back to the classic LOGIN command... + cred = credentials.GetCredential (uri, "DEFAULT"); + + ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw CreateAuthenticationException (ic); + + engine.State = ImapEngineState.Authenticated; + + id = GetSessionIdentifier (cred.UserName); + if (id != identifier) { + engine.FolderCache.Clear (); + identifier = id; + } + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the LOGIN command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// If the IMAP server supports one or more SASL authentication mechanisms, + /// then the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then LOGIN command is used as a fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (encoding, credentials, false, cancellationToken).GetAwaiter ().GetResult (); + } + + internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + engine.Uri = new Uri ($"imap://{host}:143"); + engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), false, cancellationToken).GetAwaiter ().GetResult (); + engine.TagPrefix = 'A'; + secure = false; + + if (engine.CapabilitiesVersion == 0) + engine.QueryCapabilitiesAsync (false, cancellationToken).GetAwaiter ().GetResult (); + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, 143, SecureSocketOptions.None); + + if (authenticated) + OnAuthenticatedAsync (string.Empty, false, cancellationToken).GetAwaiter ().GetResult (); + } + + internal async Task ReplayConnectAsync (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + engine.Uri = new Uri ($"imap://{host}:143"); + await engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), true, cancellationToken).ConfigureAwait (false); + engine.TagPrefix = 'A'; + secure = false; + + if (engine.CapabilitiesVersion == 0) + await engine.QueryCapabilitiesAsync (true, cancellationToken).ConfigureAwait (false); + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, 143, SecureSocketOptions.None); + + if (authenticated) + await OnAuthenticatedAsync (string.Empty, true, cancellationToken).ConfigureAwait (false); + } + + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + { + switch (options) { + default: + if (port == 0) + port = 143; + break; + case SecureSocketOptions.Auto: + switch (port) { + case 0: port = 143; goto default; + case 993: options = SecureSocketOptions.SslOnConnect; break; + default: options = SecureSocketOptions.StartTlsWhenAvailable; break; + } + break; + case SecureSocketOptions.SslOnConnect: + if (port == 0) + port = 993; + break; + } + + switch (options) { + case SecureSocketOptions.StartTlsWhenAvailable: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=when-available", host, port)); + starttls = true; + break; + case SecureSocketOptions.StartTls: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=always", host, port)); + starttls = true; + break; + case SecureSocketOptions.SslOnConnect: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imaps://{0}:{1}", host, port)); + starttls = false; + break; + default: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}", host, port)); + starttls = false; + break; + } + } + + async Task ConnectAsync (string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The ImapClient is already connected."); + + Stream stream; + bool starttls; + Uri uri; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + var socket = await ConnectSocket (host, port, doAsync, cancellationToken).ConfigureAwait (false); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + secure = true; + stream = ssl; + } else { + stream = new NetworkStream (socket, true); + secure = false; + } + + if (stream.CanTimeout) { + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + } + + try { + ProtocolLogger.LogConnect (uri); + } catch { + stream.Dispose (); + secure = false; + throw; + } + + connecting = true; + + try { + await engine.ConnectAsync (new ImapStream (stream, ProtocolLogger), doAsync, cancellationToken).ConfigureAwait (false); + } catch { + connecting = false; + secure = false; + throw; + } + + try { + // Only query the CAPABILITIES if the greeting didn't include them. + if (engine.CapabilitiesVersion == 0) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); + + if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { + var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response == ImapCommandResponse.Ok) { + try { + var tls = new SslStream (stream, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the STARTTLS command. + if (engine.CapabilitiesVersion == 1) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else if (options == SecureSocketOptions.StartTls) { + throw ImapCommandException.Create ("STARTTLS", ic); + } + } + } catch { + secure = false; + engine.Disconnect (); + throw; + } finally { + connecting = false; + } + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, port, options); + + if (authenticated) + await OnAuthenticatedAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Establish a connection to the specified IMAP server. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 993. All other values will use a default port of 143. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The ImapClient is already connected."); + + Stream network; + bool starttls; + Uri uri; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + network = ssl; + secure = true; + } else { + network = stream; + secure = false; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + try { + ProtocolLogger.LogConnect (uri); + } catch { + network.Dispose (); + secure = false; + throw; + } + + connecting = true; + + try { + await engine.ConnectAsync (new ImapStream (network, ProtocolLogger), doAsync, cancellationToken).ConfigureAwait (false); + } catch { + connecting = false; + throw; + } + + try { + // Only query the CAPABILITIES if the greeting didn't include them. + if (engine.CapabilitiesVersion == 0) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); + + if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { + var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response == ImapCommandResponse.Ok) { + var tls = new SslStream (network, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + try { + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the STARTTLS command. + if (engine.CapabilitiesVersion == 1) + await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else if (options == SecureSocketOptions.StartTls) { + throw ImapCommandException.Create ("STARTTLS", ic); + } + } + } catch { + secure = false; + engine.Disconnect (); + throw; + } finally { + connecting = false; + } + + // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. + var authenticated = engine.State == ImapEngineState.Authenticated; + + OnConnected (host, port, options); + + if (authenticated) + await OnAuthenticatedAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + } + + Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); + + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, doAsync, cancellationToken); + } + + /// + /// Establish a connection to the specified IMAP or IMAP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (socket, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Establish a connection to the specified IMAP or IMAP/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified IMAP or IMAP/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 993, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the IMAP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP command failed. + /// + /// + /// An IMAP protocol error occurred. + /// + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (stream, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DisconnectAsync (bool quit, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (!engine.IsConnected) + return; + + if (quit) { + try { + var ic = engine.QueueCommand (cancellationToken, null, "LOGOUT\r\n"); + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (ImapProtocolException) { + } catch (ImapCommandException) { + } catch (IOException) { + } + } + + disconnecting = true; + + engine.Disconnect (); + } + + /// + /// Disconnect the service. + /// + /// + /// If is true, a LOGOUT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// If set to true, a LOGOUT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + DisconnectAsync (quit, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task NoOpAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + var ic = engine.QueueCommand (cancellationToken, null, "NOOP\r\n"); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("NOOP", ic); + } + + /// + /// Ping the IMAP server to keep the connection alive. + /// + /// + /// The NOOP command is typically used to keep the connection with the IMAP server + /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the + /// IMAP server, the IMAP server will close the connection with the client, forcing the client to + /// reconnect before it can send any more commands. + /// The NOOP command also provides a great way for a client to check for new + /// messages. + /// When the IMAP server receives a NOOP command, it will reply to the client with a + /// list of pending updates such as EXISTS and RECENT counts on the currently + /// selected folder. To receive these notifications, subscribe to the + /// and events, + /// respectively. + /// For more information about the NOOP command, see + /// rfc3501. + /// + /// + /// + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOOP command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) + { + NoOpAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task IdleAsync (CancellationToken doneToken, bool doAsync, CancellationToken cancellationToken) + { + if (!doneToken.CanBeCanceled) + throw new ArgumentException ("The doneToken must be cancellable.", nameof (doneToken)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & ImapCapabilities.Idle) == 0) + throw new NotSupportedException ("The IMAP server does not support the IDLE extension."); + + if (engine.State != ImapEngineState.Selected) + throw new InvalidOperationException ("An ImapFolder has not been opened."); + + if (doneToken.IsCancellationRequested) + return; + + using (var context = new ImapIdleContext (engine, doneToken, cancellationToken)) { + var ic = engine.QueueCommand (cancellationToken, null, "IDLE\r\n"); + ic.ContinuationHandler = context.ContinuationHandler; + ic.UserData = context; + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("IDLE", ic); + } + } + + /// + /// Toggle the into the IDLE state. + /// + /// + /// When a client enters the IDLE state, the IMAP server will send + /// events to the client as they occur on the selected folder. These events + /// may include notifications of new messages arriving, expunge notifications, + /// flag changes, etc. + /// Due to the nature of the IDLE command, a folder must be selected + /// before a client can enter into the IDLE state. This can be done by + /// opening a folder using + /// + /// or any of the other variants. + /// While the IDLE command is running, no other commands may be issued until the + /// is cancelled. + /// It is especially important to cancel the + /// before cancelling the when using SSL or TLS due to + /// the fact that cannot be polled. + /// + /// + /// + /// + /// The cancellation token used to return to the non-idle state. + /// The cancellation token. + /// + /// must be cancellable (i.e. cannot be used). + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// A has not been opened. + /// + /// + /// The IMAP server does not support the IDLE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) + { + IdleAsync (doneToken, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task NotifyAsync (bool status, IList eventGroups, bool doAsync, CancellationToken cancellationToken) + { + if (eventGroups == null) + throw new ArgumentNullException (nameof (eventGroups)); + + if (eventGroups.Count == 0) + throw new ArgumentException ("No event groups specified.", nameof (eventGroups)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & ImapCapabilities.Notify) == 0) + throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); + + var command = new StringBuilder ("NOTIFY SET"); + var notifySelectedNewExpunge = false; + var args = new List (); + + if (status) + command.Append (" STATUS"); + + foreach (var group in eventGroups) { + command.Append (" "); + + group.Format (engine, command, args, ref notifySelectedNewExpunge); + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("NOTIFY", ic); + + engine.NotifySelectedNewExpunge = notifySelectedNewExpunge; + } + + /// + /// Request the specified notification events from the IMAP server. + /// + /// + /// The NOTIFY command is used to expand + /// which notifications the client wishes to be notified about, including status notifications + /// about folders other than the currently selected folder. It can also be used to automatically + /// FETCH information about new messages that have arrived in the currently selected folder. + /// This, combined with , + /// can be used to get instant notifications for changes to any of the specified folders. + /// + /// true if the server should immediately notify the client of the + /// selected folder's status; otherwise, false. + /// The specific event groups that the client would like to receive notifications for. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// One or more is invalid. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public void Notify (bool status, IList eventGroups, CancellationToken cancellationToken = default (CancellationToken)) + { + NotifyAsync (status, eventGroups, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DisableNotifyAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & ImapCapabilities.Notify) == 0) + throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); + + var ic = new ImapCommand (engine, cancellationToken, null, "NOTIFY NONE\r\n"); + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("NOTIFY", ic); + + engine.NotifySelectedNewExpunge = false; + } + + /// + /// Disable any previously requested notification events from the IMAP server. + /// + /// + /// Disables any notification events requested in a prior call to + /// . + /// request. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the NOTIFY extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the NOTIFY command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public void DisableNotify (CancellationToken cancellationToken = default (CancellationToken)) + { + DisableNotifyAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + #endregion + + #region IMailStore implementation + + /// + /// Get the personal namespaces. + /// + /// + /// The personal folder namespaces contain a user's personal mailbox folders. + /// + /// The personal namespaces. + public override FolderNamespaceCollection PersonalNamespaces { + get { return engine.PersonalNamespaces; } + } + + /// + /// Get the shared namespaces. + /// + /// + /// The shared folder namespaces contain mailbox folders that are shared with the user. + /// + /// The shared namespaces. + public override FolderNamespaceCollection SharedNamespaces { + get { return engine.SharedNamespaces; } + } + + /// + /// Get the other namespaces. + /// + /// + /// The other folder namespaces contain other mailbox folders. + /// + /// The other namespaces. + public override FolderNamespaceCollection OtherNamespaces { + get { return engine.OtherNamespaces; } + } + + /// + /// Get whether or not the mail store supports quotas. + /// + /// + /// Gets whether or not the mail store supports quotas. + /// + /// true if the mail store supports quotas; otherwise, false. + public override bool SupportsQuotas { + get { return (engine.Capabilities & ImapCapabilities.Quota) != 0; } + } + + /// + /// Get the Inbox folder. + /// + /// + /// The Inbox folder is the default folder and always exists on the server. + /// This property will only be available after the client has been authenticated. + /// + /// + /// + /// + /// The Inbox folder. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public override IMailFolder Inbox { + get { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return engine.Inbox; + } + } + + /// + /// Get the specified special folder. + /// + /// + /// Not all IMAP servers support special folders. Only IMAP servers + /// supporting the or + /// extensions may have + /// special folders. + /// + /// The folder if available; otherwise null. + /// The type of special folder. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the SPECIAL-USE nor XLIST extensions. + /// + public override IMailFolder GetFolder (SpecialFolder folder) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((Capabilities & (ImapCapabilities.SpecialUse | ImapCapabilities.XList)) == 0) + throw new NotSupportedException ("The IMAP server does not support the SPECIAL-USE nor XLIST extensions."); + + switch (folder) { + case SpecialFolder.All: return engine.All; + case SpecialFolder.Archive: return engine.Archive; + case SpecialFolder.Drafts: return engine.Drafts; + case SpecialFolder.Flagged: return engine.Flagged; + case SpecialFolder.Important: return engine.Important; + case SpecialFolder.Junk: return engine.Junk; + case SpecialFolder.Sent: return engine.Sent; + case SpecialFolder.Trash: return engine.Trash; + default: throw new ArgumentOutOfRangeException (nameof (folder)); + } + } + + /// + /// Get the folder for the specified namespace. + /// + /// + /// Gets the folder for the specified namespace. + /// + /// The folder. + /// The namespace. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder could not be found. + /// + public override IMailFolder GetFolder (FolderNamespace @namespace) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + var encodedName = engine.EncodeMailboxName (@namespace.Path); + ImapFolder folder; + + if (engine.GetCachedFolder (encodedName, out folder)) + return folder; + + throw new FolderNotFoundException (@namespace.Path); + } + + async Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken) + { + if (@namespace == null) + throw new ArgumentNullException (nameof (@namespace)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + var folders = await engine.GetFoldersAsync (@namespace, items, subscribedOnly, doAsync, cancellationToken).ConfigureAwait (false); + var list = new IMailFolder[folders.Count]; + + for (int i = 0; i < list.Length; i++) + list[i] = (IMailFolder) folders[i]; + + return list; + } + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The namespace folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the LIST or LSUB command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetFoldersAsync (@namespace, items, subscribedOnly, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the folder for the specified path. + /// + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The folder could not be found. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server replied to the IDLE command with a NO or BAD response. + /// + /// + /// The server responded with an unexpected token. + /// + public override IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)) + { + if (path == null) + throw new ArgumentNullException (nameof (path)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return engine.GetFolderAsync (path, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task GetMetadataAsync (MetadataTag tag, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + var ic = new ImapCommand (engine, cancellationToken, null, "GETMETADATA \"\" %S\r\n", tag.Id); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + var metadata = new MetadataCollection (); + ic.UserData = metadata; + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETMETADATA", ic); + + string value = null; + + for (int i = 0; i < metadata.Count; i++) { + if (metadata[i].EncodedName.Length == 0 && metadata[i].Tag.Id == tag.Id) { + value = metadata[i].Value; + metadata.RemoveAt (i); + break; + } + } + + engine.ProcessMetadataChanges (metadata); + + return value; + } + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (tag, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (tags == null) + throw new ArgumentNullException (nameof (tags)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); + + var command = new StringBuilder ("GETMETADATA \"\""); + var args = new List (); + bool hasOptions = false; + + if (options.MaxSize.HasValue || options.Depth != 0) { + command.Append (" ("); + if (options.MaxSize.HasValue) + command.AppendFormat ("MAXSIZE {0} ", options.MaxSize.Value); + if (options.Depth > 0) + command.AppendFormat ("DEPTH {0} ", options.Depth == int.MaxValue ? "infinity" : "1"); + command[command.Length - 1] = ')'; + command.Append (' '); + hasOptions = true; + } + + int startIndex = command.Length; + foreach (var tag in tags) { + command.Append (" %S"); + args.Add (tag.Id); + } + + if (hasOptions) { + command[startIndex] = '('; + command.Append (')'); + } + + command.Append ("\r\n"); + + if (args.Count == 0) + return new MetadataCollection (); + + var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + ic.UserData = new MetadataCollection (); + options.LongEntries = 0; + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETMETADATA", ic); + + if (ic.RespCodes.Count > 0 && ic.RespCodes[ic.RespCodes.Count - 1].Type == ImapResponseCodeType.Metadata) { + var metadata = (MetadataResponseCode) ic.RespCodes[ic.RespCodes.Count - 1]; + + if (metadata.SubType == MetadataResponseCodeSubType.LongEntries) + options.LongEntries = metadata.Value; + } + + return engine.FilterMetadata ((MetadataCollection) ic.UserData, string.Empty); + } + + /// + /// Gets the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (options, tags, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task SetMetadataAsync (MetadataCollection metadata, bool doAsync, CancellationToken cancellationToken) + { + if (metadata == null) + throw new ArgumentNullException (nameof (metadata)); + + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); + + if (metadata.Count == 0) + return; + + var command = new StringBuilder ("SETMETADATA \"\" ("); + var args = new List (); + + for (int i = 0; i < metadata.Count; i++) { + if (i > 0) + command.Append (' '); + + if (metadata[i].Value != null) { + command.Append ("%S %S"); + args.Add (metadata[i].Tag.Id); + args.Add (metadata[i].Value); + } else { + command.Append ("%S NIL"); + args.Add (metadata[i].Tag.Id); + } + } + command.Append (")\r\n"); + + var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); + + engine.QueueCommand (ic); + + await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SETMETADATA", ic); + } + + /// + /// Sets the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA or METADATA-SERVER extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) + { + SetMetadataAsync (metadata, false, cancellationToken).GetAwaiter ().GetResult (); + } + + #endregion + + void OnEngineMetadataChanged (object sender, MetadataChangedEventArgs e) + { + OnMetadataChanged (e.Metadata); + } + + void OnEngineFolderCreated (object sender, FolderCreatedEventArgs e) + { + OnFolderCreated (e.Folder); + } + + void OnEngineAlert (object sender, AlertEventArgs e) + { + OnAlert (e.Message); + } + + void OnEngineDisconnected (object sender, EventArgs e) + { + if (connecting) + return; + + var requested = disconnecting; + var uri = engine.Uri; + + disconnecting = false; + secure = false; + + OnDisconnected (uri.Host, uri.Port, GetSecureSocketOptions (uri), requested); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + engine.MetadataChanged -= OnEngineMetadataChanged; + engine.FolderCreated -= OnEngineFolderCreated; + engine.Disconnected -= OnEngineDisconnected; + engine.Alert -= OnEngineAlert; + engine.Dispose (); + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapCommand.cs b/src/MailKit/Net/Imap/ImapCommand.cs new file mode 100644 index 0000000..e912692 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapCommand.cs @@ -0,0 +1,918 @@ +// +// ImapCommand.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; +using MimeKit.IO; +using MimeKit.Utils; + +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Imap { + /// + /// An IMAP continuation handler. + /// + /// + /// All exceptions thrown by the handler are considered fatal and will + /// force-disconnect the connection. If a non-fatal error occurs, set + /// it on the property. + /// + delegate Task ImapContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync); + + /// + /// An IMAP untagged response handler. + /// + /// + /// Most IMAP commands return their results in untagged responses. + /// + delegate Task ImapUntaggedHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync); + + delegate void ImapCommandResetHandler (ImapCommand ic); + + /// + /// IMAP command status. + /// + enum ImapCommandStatus { + Created, + Queued, + Active, + Complete, + Error + } + + enum ImapLiteralType { + String, + //Stream, + MimeMessage + } + + enum ImapStringType { + Atom, + QString, + Literal, + Nil + } + + /// + /// An IMAP IDLE context. + /// + /// + /// An IMAP IDLE command does not work like normal commands. Unlike most commands, + /// the IDLE command does not end until the client sends a separate "DONE" command. + /// In order to facilitate this, the way this works is that the consumer of MailKit's + /// IMAP APIs provides a 'doneToken' which signals to the command-processing loop to + /// send the "DONE" command. Since, like every other IMAP command, it is also necessary to + /// provide a means of cancelling the IDLE command, it becomes necessary to link the + /// 'doneToken' and the 'cancellationToken' together. + /// + sealed class ImapIdleContext : IDisposable + { + static readonly byte[] DoneCommand = Encoding.ASCII.GetBytes ("DONE\r\n"); + CancellationTokenRegistration registration; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP engine. + /// The done token. + /// The cancellation token. + public ImapIdleContext (ImapEngine engine, CancellationToken doneToken, CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + DoneToken = doneToken; + Engine = engine; + } + + /// + /// Get the engine. + /// + /// + /// Gets the engine. + /// + /// The engine. + public ImapEngine Engine { + get; private set; + } + + /// + /// Get the cancellation token. + /// + /// + /// Get the cancellation token. + /// + /// The cancellation token. + public CancellationToken CancellationToken { + get; private set; + } + + /// + /// Get the done token. + /// + /// + /// Gets the done token. + /// + /// The done token. + public CancellationToken DoneToken { + get; private set; + } + +#if false + /// + /// Get whether or not cancellation has been requested. + /// + /// + /// Gets whether or not cancellation has been requested. + /// + /// true if cancellation has been requested; otherwise, false. + public bool IsCancellationRequested { + get { return CancellationToken.IsCancellationRequested; } + } + + /// + /// Get whether or not the IDLE command should be ended. + /// + /// + /// Gets whether or not the IDLE command should be ended. + /// + /// true if the IDLE command should end; otherwise, false. + public bool IsDoneRequested { + get { return DoneToken.IsCancellationRequested; } + } +#endif + + void IdleComplete () + { + if (Engine.State == ImapEngineState.Idle) { + try { + Engine.Stream.Write (DoneCommand, 0, DoneCommand.Length, CancellationToken); + Engine.Stream.Flush (CancellationToken); + } catch { + return; + } + + Engine.State = ImapEngineState.Selected; + } + } + + /// + /// Callback method to be used as the ImapCommand's ContinuationHandler. + /// + /// + /// Callback method to be used as the ImapCommand's ContinuationHandler. + /// + /// The ImapEngine. + /// The ImapCommand. + /// The text. + /// true if the command is being run asynchronously; otherwise, false. + /// + public Task ContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync) + { + Engine.State = ImapEngineState.Idle; + + registration = DoneToken.Register (IdleComplete); + + return Task.FromResult (true); + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the + /// so the garbage collector can reclaim the memory that the + /// was occupying. + public void Dispose () + { + registration.Dispose (); + } + } + + /// + /// An IMAP literal object. + /// + /// + /// The literal can be a string, byte[], Stream, or a MimeMessage. + /// + class ImapLiteral + { + public readonly ImapLiteralType Type; + public readonly object Literal; + readonly FormatOptions format; + readonly Action update; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The formatting options. + /// The message. + /// The progress update action. + public ImapLiteral (FormatOptions options, MimeMessage message, Action action = null) + { + format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + + update = action; + + Type = ImapLiteralType.MimeMessage; + Literal = message; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The formatting options. + /// The literal. + public ImapLiteral (FormatOptions options, byte[] literal) + { + format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + + Type = ImapLiteralType.String; + Literal = literal; + } + + /// + /// Get the length of the literal, in bytes. + /// + /// + /// Gets the length of the literal, in bytes. + /// + /// The length. + public long Length { + get { + if (Type == ImapLiteralType.String) + return ((byte[]) Literal).Length; + + using (var measure = new MeasuringStream ()) { + //if (Type == ImapLiteralType.Stream) { + // var stream = (Stream) Literal; + // stream.CopyTo (measure, 4096); + // stream.Position = 0; + + // return measure.Length; + //} + + ((MimeMessage) Literal).WriteTo (format, measure); + + return measure.Length; + } + } + } + + /// + /// Write the literal to the specified stream. + /// + /// + /// Writes the literal to the specified stream. + /// + /// The stream. + /// Whether the literal should be written asynchronously or not. + /// The cancellation token. + public async Task WriteToAsync (ImapStream stream, bool doAsync, CancellationToken cancellationToken) + { + if (Type == ImapLiteralType.String) { + var bytes = (byte[]) Literal; + + if (doAsync) { + await stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + stream.Write (bytes, 0, bytes.Length, cancellationToken); + stream.Flush (cancellationToken); + } + return; + } + + //if (Type == ImapLiteralType.Stream) { + // var literal = (Stream) Literal; + // var buf = new byte[4096]; + // int nread; + + // if (doAsync) { + // while ((nread = await literal.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0) + // await stream.WriteAsync (buf, 0, nread, cancellationToken).ConfigureAwait (false); + + // await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + // } else { + // while ((nread = literal.Read (buf, 0, buf.Length)) > 0) + // stream.Write (buf, 0, nread, cancellationToken); + + // stream.Flush (cancellationToken); + // } + // return; + //} + + var message = (MimeMessage) Literal; + + using (var s = new ProgressStream (stream, update)) { + if (doAsync) { + await message.WriteToAsync (format, s, cancellationToken).ConfigureAwait (false); + await s.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + message.WriteTo (format, s, cancellationToken); + s.Flush (cancellationToken); + } + } + } + } + + /// + /// A partial IMAP command. + /// + /// + /// IMAP commands that contain literal strings are broken up into multiple parts + /// in case the IMAP server does not support the LITERAL+ extension. These parts + /// are then sent individually as we receive "+" responses from the server. + /// + class ImapCommandPart + { + public readonly byte[] Command; + public readonly ImapLiteral Literal; + public readonly bool WaitForContinuation; + + public ImapCommandPart (byte[] command, ImapLiteral literal, bool wait = true) + { + WaitForContinuation = wait; + Command = command; + Literal = literal; + } + } + + /// + /// An IMAP command. + /// + class ImapCommand + { + static readonly byte[] UTF8LiteralTokenPrefix = Encoding.ASCII.GetBytes ("UTF8 (~{"); + static readonly byte[] LiteralTokenSuffix = { (byte) '}', (byte) '\r', (byte) '\n' }; + static readonly byte[] Nil = { (byte) 'N', (byte) 'I', (byte) 'L' }; + static readonly byte[] NewLine = { (byte) '\r', (byte) '\n' }; + static readonly byte[] LiteralTokenPrefix = { (byte) '{' }; + + public Dictionary UntaggedHandlers { get; private set; } + public ImapContinuationHandler ContinuationHandler { get; set; } + public CancellationToken CancellationToken { get; private set; } + public ImapCommandStatus Status { get; internal set; } + public ImapCommandResponse Response { get; internal set; } + public ITransferProgress Progress { get; internal set; } + public Exception Exception { get; internal set; } + public readonly List RespCodes; + public string ResponseText { get; internal set; } + public ImapFolder Folder { get; private set; } + public object UserData { get; internal set; } + public bool ListReturnsSubscribed { get; internal set; } + public bool Logout { get; private set; } + public bool Lsub { get; internal set; } + public string Tag { get; private set; } + public bool Bye { get; internal set; } + + readonly List parts = new List (); + readonly ImapEngine Engine; + long totalSize, nwritten; + int current; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP engine that will be sending the command. + /// The cancellation token. + /// The IMAP folder that the command operates on. + /// The formatting options. + /// The command format. + /// The command arguments. + public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) + { + UntaggedHandlers = new Dictionary (StringComparer.OrdinalIgnoreCase); + Logout = format.Equals ("LOGOUT\r\n", StringComparison.Ordinal); + RespCodes = new List (); + CancellationToken = cancellationToken; + Response = ImapCommandResponse.None; + Status = ImapCommandStatus.Created; + Engine = engine; + Folder = folder; + + using (var builder = new MemoryStream ()) { + byte[] buf, utf8 = new byte[8]; + int argc = 0; + string str; + + for (int i = 0; i < format.Length; i++) { + if (format[i] == '%') { + switch (format[++i]) { + case '%': // a literal % + builder.WriteByte ((byte) '%'); + break; + case 'd': // an integer + str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); + buf = Encoding.ASCII.GetBytes (str); + builder.Write (buf, 0, buf.Length); + break; + case 'u': // an unsigned integer + str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); + buf = Encoding.ASCII.GetBytes (str); + builder.Write (buf, 0, buf.Length); + break; + case 's': + str = (string) args[argc++]; + buf = Encoding.ASCII.GetBytes (str); + builder.Write (buf, 0, buf.Length); + break; + case 'F': // an ImapFolder + var utf7 = ((ImapFolder) args[argc++]).EncodedName; + AppendString (options, true, builder, utf7); + break; + case 'L': // a MimeMessage or a byte[] + var arg = args[argc++]; + ImapLiteral literal; + byte[] prefix; + + if (arg is MimeMessage message) { + prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; + literal = new ImapLiteral (options, message, UpdateProgress); + } else { + literal = new ImapLiteral (options, (byte[]) arg); + prefix = LiteralTokenPrefix; + } + + var length = literal.Length; + bool wait = true; + + builder.Write (prefix, 0, prefix.Length); + buf = Encoding.ASCII.GetBytes (length.ToString (CultureInfo.InvariantCulture)); + builder.Write (buf, 0, buf.Length); + + if (CanUseNonSynchronizedLiteral (Engine, length)) { + builder.WriteByte ((byte) '+'); + wait = false; + } + + builder.Write (LiteralTokenSuffix, 0, LiteralTokenSuffix.Length); + + totalSize += length; + + parts.Add (new ImapCommandPart (builder.ToArray (), literal, wait)); + builder.SetLength (0); + + if (prefix == UTF8LiteralTokenPrefix) + builder.WriteByte ((byte) ')'); + break; + case 'S': // a string which may need to be quoted or made into a literal + AppendString (options, true, builder, (string) args[argc++]); + break; + case 'Q': // similar to %S but string must be quoted at a minimum + AppendString (options, false, builder, (string) args[argc++]); + break; + default: + throw new FormatException (); + } + } else if (format[i] < 128) { + builder.WriteByte ((byte) format[i]); + } else { + int nchars = char.IsSurrogate (format[i]) ? 2 : 1; + int nbytes = Encoding.UTF8.GetBytes (format, i, nchars, utf8, 0); + builder.Write (utf8, 0, nbytes); + i += nchars - 1; + } + } + + parts.Add (new ImapCommandPart (builder.ToArray (), null)); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP engine that will be sending the command. + /// The cancellation token. + /// The IMAP folder that the command operates on. + /// The command format. + /// The command arguments. + public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) + : this (engine, cancellationToken, folder, FormatOptions.Default, format, args) + { + } + + internal static int EstimateCommandLength (ImapEngine engine, FormatOptions options, string format, params object[] args) + { + const int EstimatedTagLength = 10; + var eoln = false; + int length = 0; + int argc = 0; + string str; + + for (int i = 0; i < format.Length; i++) { + if (format[i] == '%') { + switch (format[++i]) { + case '%': // a literal % + length++; + break; + case 'd': // an integer + str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); + length += str.Length; + break; + case 'u': // an unsigned integer + str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); + length += str.Length; + break; + case 's': + str = (string) args[argc++]; + length += str.Length; + break; + case 'F': // an ImapFolder + var utf7 = ((ImapFolder) args[argc++]).EncodedName; + length += EstimateStringLength (engine, options, true, utf7, out eoln); + break; + case 'L': // a MimeMessage or a byte[] + var arg = args[argc++]; + byte[] prefix; + long len; + + if (arg is MimeMessage message) { + prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; + var literal = new ImapLiteral (options, message, null); + len = literal.Length; + } else { + len = ((byte[]) arg).Length; + prefix = LiteralTokenPrefix; + } + + length += prefix.Length; + length += Encoding.ASCII.GetByteCount (len.ToString (CultureInfo.InvariantCulture)); + + if (CanUseNonSynchronizedLiteral (engine, len)) + length++; + + length += LiteralTokenSuffix.Length; + + if (prefix == UTF8LiteralTokenPrefix) + length++; + + eoln = true; + break; + case 'S': // a string which may need to be quoted or made into a literal + length += EstimateStringLength (engine, options, true, (string) args[argc++], out eoln); + break; + case 'Q': // similar to %S but string must be quoted at a minimum + length += EstimateStringLength (engine, options, false, (string) args[argc++], out eoln); + break; + default: + throw new FormatException (); + } + + if (eoln) + break; + } else { + length++; + } + } + + return length + EstimatedTagLength; + } + + internal static int EstimateCommandLength (ImapEngine engine, string format, params object[] args) + { + return EstimateCommandLength (engine, FormatOptions.Default, format, args); + } + + void UpdateProgress (int n) + { + nwritten += n; + + if (Progress != null) + Progress.Report (nwritten, totalSize); + } + + static bool IsAtom (char c) + { + return c < 128 && !char.IsControl (c) && "(){ \t%*\\\"]".IndexOf (c) == -1; + } + + static bool IsQuotedSafe (ImapEngine engine, char c) + { + return (c < 128 || engine.UTF8Enabled) && !char.IsControl (c); + } + + internal static ImapStringType GetStringType (ImapEngine engine, string value, bool allowAtom) + { + var type = allowAtom ? ImapStringType.Atom : ImapStringType.QString; + + if (value == null) + return ImapStringType.Nil; + + if (value.Length == 0) + return ImapStringType.QString; + + for (int i = 0; i < value.Length; i++) { + if (!IsAtom (value[i])) { + if (!IsQuotedSafe (engine, value[i])) + return ImapStringType.Literal; + + type = ImapStringType.QString; + } + } + + return type; + } + + static bool CanUseNonSynchronizedLiteral (ImapEngine engine, long length) + { + return (engine.Capabilities & ImapCapabilities.LiteralPlus) != 0 || + (length <= 4096 && (engine.Capabilities & ImapCapabilities.LiteralMinus) != 0); + } + + static int EstimateStringLength (ImapEngine engine, FormatOptions options, bool allowAtom, string value, out bool eoln) + { + eoln = false; + + switch (GetStringType (engine, value, allowAtom)) { + case ImapStringType.Literal: + var literal = Encoding.UTF8.GetByteCount (value); + var plus = CanUseNonSynchronizedLiteral (engine, literal); + int length = "{}\r\n".Length; + + length += literal.ToString (CultureInfo.InvariantCulture).Length; + if (plus) + length++; + + eoln = true; + + return length++; + case ImapStringType.QString: + return Encoding.UTF8.GetByteCount (MimeUtils.Quote (value)); + case ImapStringType.Nil: + return Nil.Length; + default: + return value.Length; + } + } + + void AppendString (FormatOptions options, bool allowAtom, MemoryStream builder, string value) + { + byte[] buf; + + switch (GetStringType (Engine, value, allowAtom)) { + case ImapStringType.Literal: + var literal = Encoding.UTF8.GetBytes (value); + var plus = CanUseNonSynchronizedLiteral (Engine, literal.Length); + var length = literal.Length.ToString (CultureInfo.InvariantCulture); + buf = Encoding.ASCII.GetBytes (length); + + builder.WriteByte ((byte) '{'); + builder.Write (buf, 0, buf.Length); + if (plus) + builder.WriteByte ((byte) '+'); + builder.WriteByte ((byte) '}'); + builder.WriteByte ((byte) '\r'); + builder.WriteByte ((byte) '\n'); + + if (plus) { + builder.Write (literal, 0, literal.Length); + } else { + parts.Add (new ImapCommandPart (builder.ToArray (), new ImapLiteral (options, literal))); + builder.SetLength (0); + } + break; + case ImapStringType.QString: + buf = Encoding.UTF8.GetBytes (MimeUtils.Quote (value)); + builder.Write (buf, 0, buf.Length); + break; + case ImapStringType.Atom: + buf = Encoding.UTF8.GetBytes (value); + builder.Write (buf, 0, buf.Length); + break; + case ImapStringType.Nil: + builder.Write (Nil, 0, Nil.Length); + break; + } + } + + /// + /// Registers the untagged handler for the specified atom token. + /// + /// The atom token. + /// The handler. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// Untagged handlers must be registered before the command has been queued. + /// + public void RegisterUntaggedHandler (string atom, ImapUntaggedHandler handler) + { + if (atom == null) + throw new ArgumentNullException (nameof (atom)); + + if (handler == null) + throw new ArgumentNullException (nameof (handler)); + + if (Status != ImapCommandStatus.Created) + throw new InvalidOperationException ("Untagged handlers must be registered before the command has been queued."); + + UntaggedHandlers.Add (atom, handler); + } + + /// + /// Sends the next part of the command to the server. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task StepAsync (bool doAsync) + { + var supportsLiteralPlus = (Engine.Capabilities & ImapCapabilities.LiteralPlus) != 0; + var idle = UserData as ImapIdleContext; + var result = ImapCommandResponse.None; + ImapToken token; + + // construct and write the command tag if this is the initial state + if (current == 0) { + Tag = string.Format (CultureInfo.InvariantCulture, "{0}{1:D8}", Engine.TagPrefix, Engine.Tag++); + + var buf = Encoding.ASCII.GetBytes (Tag + " "); + + if (doAsync) + await Engine.Stream.WriteAsync (buf, 0, buf.Length, CancellationToken).ConfigureAwait (false); + else + Engine.Stream.Write (buf, 0, buf.Length, CancellationToken); + } + + do { + var command = parts[current].Command; + + if (doAsync) + await Engine.Stream.WriteAsync (command, 0, command.Length, CancellationToken).ConfigureAwait (false); + else + Engine.Stream.Write (command, 0, command.Length, CancellationToken); + + // if the server doesn't support LITERAL+ (or LITERAL-), we'll need to wait + // for a "+" response before writing out the any literals... + if (parts[current].WaitForContinuation) + break; + + // otherwise, we can write out any and all literal tokens we have... + await parts[current].Literal.WriteToAsync (Engine.Stream, doAsync, CancellationToken).ConfigureAwait (false); + + if (current + 1 >= parts.Count) + break; + + current++; + } while (true); + + if (doAsync) + await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); + else + Engine.Stream.Flush (CancellationToken); + + // now we need to read the response... + do { + if (Engine.State == ImapEngineState.Idle) { + int timeout = Timeout.Infinite; + + if (Engine.Stream.CanTimeout) { + timeout = Engine.Stream.ReadTimeout; + Engine.Stream.ReadTimeout = Timeout.Infinite; + } + + try { + token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); + } finally { + if (Engine.Stream.IsConnected && Engine.Stream.CanTimeout) + Engine.Stream.ReadTimeout = timeout; + } + } else { + token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.Atom && token.Value.ToString () == "+") { + // we've gotten a continuation response from the server + var text = (await Engine.ReadLineAsync (doAsync, CancellationToken).ConfigureAwait (false)).Trim (); + + // if we've got a Literal pending, the '+' means we can send it now... + if (!supportsLiteralPlus && parts[current].Literal != null) { + await parts[current].Literal.WriteToAsync (Engine.Stream, doAsync, CancellationToken).ConfigureAwait (false); + break; + } + + if (ContinuationHandler != null) { + await ContinuationHandler (Engine, this, text, doAsync).ConfigureAwait (false); + } else if (doAsync) { + await Engine.Stream.WriteAsync (NewLine, 0, NewLine.Length, CancellationToken).ConfigureAwait (false); + await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); + } else { + Engine.Stream.Write (NewLine, 0, NewLine.Length, CancellationToken); + Engine.Stream.Flush (CancellationToken); + } + } else if (token.Type == ImapTokenType.Asterisk) { + // we got an untagged response, let the engine handle this... + await Engine.ProcessUntaggedResponseAsync (doAsync, CancellationToken).ConfigureAwait (false); + } else if (token.Type == ImapTokenType.Atom && (string) token.Value == Tag) { + // the next token should be "OK", "NO", or "BAD" + token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, "Syntax error in tagged response. Unexpected token: {0}", token); + + string atom = (string) token.Value; + + switch (atom) { + case "BAD": result = ImapCommandResponse.Bad; break; + case "OK": result = ImapCommandResponse.Ok; break; + case "NO": result = ImapCommandResponse.No; break; + default: throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. Unexpected token: {0}", token); + } + + token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); + if (token.Type == ImapTokenType.OpenBracket) { + var code = await Engine.ParseResponseCodeAsync (true, doAsync, CancellationToken).ConfigureAwait (false); + RespCodes.Add (code); + break; + } + + if (token.Type != ImapTokenType.Eoln) { + // consume the rest of the line... + var line = await Engine.ReadLineAsync (doAsync, CancellationToken).ConfigureAwait (false); + ResponseText = (((string) token.Value) + line).TrimEnd (); + break; + } + } else if (token.Type == ImapTokenType.OpenBracket) { + // Note: this is a work-around for broken IMAP servers like Office365.com that + // return RESP-CODES that are not preceded by "* OK " such as the example in + // issue #115 (https://github.com/jstedfast/MailKit/issues/115). + var code = await Engine.ParseResponseCodeAsync (false, doAsync, CancellationToken).ConfigureAwait (false); + RespCodes.Add (code); + } else { + // no clue what we got... + throw ImapEngine.UnexpectedToken ("Syntax error in response. Unexpected token: {0}", token); + } + } while (Status == ImapCommandStatus.Active); + + if (Status == ImapCommandStatus.Active) { + current++; + + if (current >= parts.Count || result != ImapCommandResponse.None) { + Status = ImapCommandStatus.Complete; + Response = result; + return false; + } + + return true; + } + + return false; + } + } +} diff --git a/src/MailKit/Net/Imap/ImapCommandException.cs b/src/MailKit/Net/Imap/ImapCommandException.cs new file mode 100644 index 0000000..1cc2684 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapCommandException.cs @@ -0,0 +1,190 @@ +// +// ImapCommandException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Imap { + /// + /// An exception that is thrown when an IMAP command returns NO or BAD. + /// + /// + /// The exception that is thrown when an IMAP command fails. Unlike a , + /// a does not require the to be reconnected. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ImapCommandException : CommandException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected ImapCommandException (SerializationInfo info, StreamingContext context) : base (info, context) + { + Response = (ImapCommandResponse) info.GetValue ("Response", typeof (ImapCommandResponse)); + ResponseText = info.GetString ("ResponseText"); + } +#endif + + /// + /// Create a new based on the specified command name and state. + /// + /// + /// Create a new based on the specified command name and state. + /// + /// A new command exception. + /// The command name. + /// The command state. + internal static ImapCommandException Create (string command, ImapCommand ic) + { + var result = ic.Response.ToString ().ToUpperInvariant (); + string message, reason = null; + + if (string.IsNullOrEmpty (ic.ResponseText)) { + for (int i = ic.RespCodes.Count - 1; i >= 0; i--) { + if (ic.RespCodes[i].IsError && !string.IsNullOrEmpty (ic.RespCodes[i].Message)) { + reason = ic.RespCodes[i].Message; + break; + } + } + } else { + reason = ic.ResponseText; + } + + if (!string.IsNullOrEmpty (reason)) + message = string.Format ("The IMAP server replied to the '{0}' command with a '{1}' response: {2}", command, result, reason); + else + message = string.Format ("The IMAP server replied to the '{0}' command with a '{1}' response.", command, result); + + return ic.Exception != null ? new ImapCommandException (ic.Response, reason, message, ic.Exception) : new ImapCommandException (ic.Response, reason, message); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP command response. + /// The error message. + /// The human-readable response text. + /// The inner exception. + public ImapCommandException (ImapCommandResponse response, string responseText, string message, Exception innerException) : base (message, innerException) + { + ResponseText = responseText; + Response = response; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP command response. + /// The human-readable response text. + /// The error message. + public ImapCommandException (ImapCommandResponse response, string responseText, string message) : base (message) + { + ResponseText = responseText; + Response = response; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The IMAP command response. + /// The human-readable response text. + public ImapCommandException (ImapCommandResponse response, string responseText) + { + ResponseText = responseText; + Response = response; + } + + /// + /// Gets the IMAP command response. + /// + /// + /// Gets the IMAP command response. + /// + /// The IMAP command response. + public ImapCommandResponse Response { + get; private set; + } + + /// + /// Gets the human-readable IMAP command response text. + /// + /// + /// Gets the human-readable IMAP command response text. + /// + /// The response text. + public string ResponseText { + get; private set; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("Response", Response, typeof (ImapCommandResponse)); + info.AddValue ("ResponseText", ResponseText); + } +#endif + } +} diff --git a/src/MailKit/Net/Imap/ImapCommandResponse.cs b/src/MailKit/Net/Imap/ImapCommandResponse.cs new file mode 100644 index 0000000..1a38ad4 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapCommandResponse.cs @@ -0,0 +1,55 @@ +// +// ImapCommandResult.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Imap { + /// + /// An enumeration of possible IMAP command responses. + /// + /// + /// An enumeration of possible IMAP command responses. + /// + public enum ImapCommandResponse { + /// + /// No IMAP command response yet. + /// + None, + + /// + /// The command resulted in an "OK" response. + /// + Ok, + + /// + /// The command resulted in a "NO" response. + /// + No, + + /// + /// The command resulted in a "BAD" response. + /// + Bad + } +} diff --git a/src/MailKit/Net/Imap/ImapEncoding.cs b/src/MailKit/Net/Imap/ImapEncoding.cs new file mode 100644 index 0000000..831c64f --- /dev/null +++ b/src/MailKit/Net/Imap/ImapEncoding.cs @@ -0,0 +1,155 @@ +// +// ImapEncoding.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Text; + +namespace MailKit.Net.Imap { + static class ImapEncoding + { + const string utf7_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,"; + + static readonly byte[] utf7_rank = { + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255, 62, 63,255,255,255, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255, + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, + 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255, + }; + + public static string Decode (string text) + { + var decoded = new StringBuilder (); + bool shifted = false; + int bits = 0, v = 0; + int index = 0; + char c; + + while (index < text.Length) { + c = text[index++]; + + if (shifted) { + if (c == '-') { + // shifted back out of modified UTF-7 + shifted = false; + bits = v = 0; + } else if (c > 127) { + // invalid UTF-7 + return text; + } else { + byte rank = utf7_rank[(byte) c]; + + if (rank == 0xff) { + // invalid UTF-7 + return text; + } + + v = (v << 6) | rank; + bits += 6; + + if (bits >= 16) { + char u = (char) ((v >> (bits - 16)) & 0xffff); + decoded.Append (u); + bits -= 16; + } + } + } else if (c == '&' && index < text.Length) { + if (text[index] == '-') { + decoded.Append ('&'); + index++; + } else { + // shifted into modified UTF-7 + shifted = true; + } + } else { + decoded.Append (c); + } + } + + return decoded.ToString (); + } + + static void Utf7ShiftOut (StringBuilder output, int u, int bits) + { + if (bits > 0) { + int x = (u << (6 - bits)) & 0x3f; + output.Append (utf7_alphabet[x]); + } + + output.Append ('-'); + } + + public static string Encode (string text) + { + var encoded = new StringBuilder (); + bool shifted = false; + int bits = 0, u = 0; + + for (int index = 0; index < text.Length; index++) { + char c = text[index]; + + if (c >= 0x20 && c < 0x7f) { + // characters with octet values 0x20-0x25 and 0x27-0x7e + // represent themselves while 0x26 ("&") is represented + // by the two-octet sequence "&-" + + if (shifted) { + Utf7ShiftOut (encoded, u, bits); + shifted = false; + bits = 0; + } + + if (c == 0x26) + encoded.Append ("&-"); + else + encoded.Append (c); + } else { + // base64 encode + if (!shifted) { + encoded.Append ('&'); + shifted = true; + } + + u = (u << 16) | (c & 0xffff); + bits += 16; + + while (bits >= 6) { + int x = (u >> (bits - 6)) & 0x3f; + encoded.Append (utf7_alphabet[x]); + bits -= 6; + } + } + } + + if (shifted) + Utf7ShiftOut (encoded, u, bits); + + return encoded.ToString (); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapEngine.cs b/src/MailKit/Net/Imap/ImapEngine.cs new file mode 100644 index 0000000..6765c35 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapEngine.cs @@ -0,0 +1,2905 @@ +// +// ImapEngine.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit.Net.Imap { + delegate ImapFolder CreateImapFolderDelegate (ImapFolderConstructorArgs args); + + /// + /// The state of the . + /// + enum ImapEngineState { + /// + /// The ImapEngine is in the disconnected state. + /// + Disconnected, + + /// + /// The ImapEngine is in the process of connecting. + /// + Connecting, + + /// + /// The ImapEngine is connected but not yet authenticated. + /// + Connected, + + /// + /// The ImapEngine is in the authenticated state. + /// + Authenticated, + + /// + /// The ImapEngine is in the selected state. + /// + Selected, + + /// + /// The ImapEngine is in the IDLE state. + /// + Idle + } + + enum ImapProtocolVersion { + Unknown, + IMAP4, + IMAP4rev1 + } + + enum ImapUntaggedResult { + Ok, + No, + Bad, + Handled + } + + enum ImapQuirksMode { + None, + Courier, + Cyrus, + Domino, + Dovecot, + Exchange, + GMail, + ProtonMail, + SmarterMail, + SunMicrosystems, + UW, + Yahoo, + Yandex + } + + class ImapFolderNameComparer : IEqualityComparer + { + public char DirectorySeparator; + + public ImapFolderNameComparer (char directorySeparator) + { + DirectorySeparator = directorySeparator; + } + + public bool Equals (string x, string y) + { + x = ImapUtils.CanonicalizeMailboxName (x, DirectorySeparator); + y = ImapUtils.CanonicalizeMailboxName (y, DirectorySeparator); + + return x == y; + } + + public int GetHashCode (string obj) + { + return ImapUtils.CanonicalizeMailboxName (obj, DirectorySeparator).GetHashCode (); + } + } + + /// + /// An IMAP command engine. + /// + class ImapEngine : IDisposable + { + internal const string GenericUntaggedResponseSyntaxErrorFormat = "Syntax error in untagged {0} response. Unexpected token: {1}"; + internal const string GenericItemSyntaxErrorFormat = "Syntax error in {0}. Unexpected token: {1}"; + internal const string FetchBodySyntaxErrorFormat = "Syntax error in BODY. Unexpected token: {0}"; + const string GenericResponseCodeSyntaxErrorFormat = "Syntax error in {0} response code. Unexpected token: {1}"; + const string GreetingSyntaxErrorFormat = "Syntax error in IMAP server greeting. Unexpected token: {0}"; + + internal static readonly Encoding Latin1; + internal static readonly Encoding UTF8; + static int TagPrefixIndex; + + internal readonly Dictionary FolderCache; + readonly CreateImapFolderDelegate createImapFolder; + readonly ImapFolderNameComparer cacheComparer; + internal ImapQuirksMode QuirksMode; + readonly List queue; + internal char TagPrefix; + ImapCommand current; + MimeParser parser; + internal int Tag; + bool disposed; + + static ImapEngine () + { + UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); + + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.GetEncoding (1252); + } + } + + public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate) + { + cacheComparer = new ImapFolderNameComparer ('.'); + + FolderCache = new Dictionary (cacheComparer); + ThreadingAlgorithms = new HashSet (); + AuthenticationMechanisms = new HashSet (StringComparer.Ordinal); + CompressionAlgorithms = new HashSet (StringComparer.Ordinal); + SupportedContexts = new HashSet (StringComparer.Ordinal); + SupportedCharsets = new HashSet (StringComparer.OrdinalIgnoreCase); + Rights = new AccessRights (); + + PersonalNamespaces = new FolderNamespaceCollection (); + SharedNamespaces = new FolderNamespaceCollection (); + OtherNamespaces = new FolderNamespaceCollection (); + + ProtocolVersion = ImapProtocolVersion.Unknown; + createImapFolder = createImapFolderDelegate; + Capabilities = ImapCapabilities.None; + QuirksMode = ImapQuirksMode.None; + queue = new List (); + } + + /// + /// Get the authentication mechanisms supported by the IMAP server. + /// + /// + /// The authentication mechanisms are queried durring the + /// method. + /// + /// The authentication mechanisms. + public HashSet AuthenticationMechanisms { + get; private set; + } + + /// + /// Get the compression algorithms supported by the IMAP server. + /// + /// + /// The compression algorithms are populated by the + /// method. + /// + /// The compression algorithms. + public HashSet CompressionAlgorithms { + get; private set; + } + + /// + /// Get the threading algorithms supported by the IMAP server. + /// + /// + /// The threading algorithms are populated by the + /// method. + /// + /// The threading algorithms. + public HashSet ThreadingAlgorithms { + get; private set; + } + + /// + /// Gets the append limit supported by the IMAP server. + /// + /// + /// Gets the append limit supported by the IMAP server. + /// + /// The append limit. + public uint? AppendLimit { + get; private set; + } + + /// + /// Gets the I18NLEVEL supported by the IMAP server. + /// + /// + /// Gets the I18NLEVEL supported by the IMAP server. + /// + /// The internationalization level. + public int I18NLevel { + get; private set; + } + + /// + /// Get the capabilities supported by the IMAP server. + /// + /// + /// The capabilities will not be known until a successful connection + /// has been made via the method. + /// + /// The capabilities. + public ImapCapabilities Capabilities { + get; set; + } + + /// + /// Indicates whether or not the engine is busy processing commands. + /// + /// + /// Indicates whether or not the engine is busy processing commands. + /// + /// true if th e engine is busy processing commands; otherwise, false. + internal bool IsBusy { + get { return current != null; } + } + + /// + /// Get the capabilities version. + /// + /// + /// Every time the engine receives an untagged CAPABILITIES + /// response from the server, it increments this value. + /// + /// The capabilities version. + public int CapabilitiesVersion { + get; private set; + } + + /// + /// Get the IMAP protocol version. + /// + /// + /// Gets the IMAP protocol version. + /// + /// The IMAP protocol version. + public ImapProtocolVersion ProtocolVersion { + get; private set; + } + + /// + /// Get the rights specified in the capabilities. + /// + /// + /// Gets the rights specified in the capabilities. + /// + /// The rights. + public AccessRights Rights { + get; private set; + } + + /// + /// Get the supported charsets. + /// + /// + /// Gets the supported charsets. + /// + /// The supported charsets. + public HashSet SupportedCharsets { + get; private set; + } + + /// + /// Get the supported contexts. + /// + /// + /// Gets the supported contexts. + /// + /// The supported contexts. + public HashSet SupportedContexts { + get; private set; + } + + /// + /// Get whether or not the QRESYNC feature has been enabled. + /// + /// + /// Gets whether or not the QRESYNC feature has been enabled. + /// + /// true if the QRESYNC feature has been enabled; otherwise, false. + public bool QResyncEnabled { + get; internal set; + } + + /// + /// Get whether or not the UTF8=ACCEPT feature has been enabled. + /// + /// + /// Gets whether or not the UTF8=ACCEPT feature has been enabled. + /// + /// true if the UTF8=ACCEPT feature has been enabled; otherwise, false. + public bool UTF8Enabled { + get; internal set; + } + + /// + /// Get the URI of the IMAP server. + /// + /// + /// Gets the URI of the IMAP server. + /// + /// The URI of the IMAP server. + public Uri Uri { + get; internal set; + } + + /// + /// Get the underlying IMAP stream. + /// + /// + /// Gets the underlying IMAP stream. + /// + /// The IMAP stream. + public ImapStream Stream { + get; private set; + } + + /// + /// Get or sets the state of the engine. + /// + /// + /// Gets or sets the state of the engine. + /// + /// The engine state. + public ImapEngineState State { + get; internal set; + } + + /// + /// Get whether or not the engine is currently connected to a IMAP server. + /// + /// + /// Gets whether or not the engine is currently connected to a IMAP server. + /// + /// true if the engine is connected; otherwise, false. + public bool IsConnected { + get { return Stream != null && Stream.IsConnected; } + } + + /// + /// Gets the personal folder namespaces. + /// + /// + /// Gets the personal folder namespaces. + /// + /// The personal folder namespaces. + public FolderNamespaceCollection PersonalNamespaces { + get; private set; + } + + /// + /// Gets the shared folder namespaces. + /// + /// + /// Gets the shared folder namespaces. + /// + /// The shared folder namespaces. + public FolderNamespaceCollection SharedNamespaces { + get; private set; + } + + /// + /// Gets the other folder namespaces. + /// + /// + /// Gets the other folder namespaces. + /// + /// The other folder namespaces. + public FolderNamespaceCollection OtherNamespaces { + get; private set; + } + + /// + /// Gets the selected folder. + /// + /// + /// Gets the selected folder. + /// + /// The selected folder. + public ImapFolder Selected { + get; internal set; + } + + /// + /// Gets a value indicating whether the engine is disposed. + /// + /// + /// Gets a value indicating whether the engine is disposed. + /// + /// true if the engine is disposed; otherwise, false. + public bool IsDisposed { + get { return disposed; } + } + + /// + /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. + /// + /// + /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. This is the case when the client has asked for MessageNew or MessageExpunge events on the SELECTED mailbox. + /// + /// true if the use of indexes and * is prevented; otherwise, false. + internal bool NotifySelectedNewExpunge { + get; set; + } + + #region Special Folders + + /// + /// Gets the Inbox folder. + /// + /// The Inbox folder. + public ImapFolder Inbox { + get; private set; + } + + /// + /// Gets the special folder containing an aggregate of all messages. + /// + /// The folder containing all messages. + public ImapFolder All { + get; private set; + } + + /// + /// Gets the special archive folder. + /// + /// The archive folder. + public ImapFolder Archive { + get; private set; + } + + /// + /// Gets the special folder containing drafts. + /// + /// The drafts folder. + public ImapFolder Drafts { + get; private set; + } + + /// + /// Gets the special folder containing flagged messages. + /// + /// The flagged folder. + public ImapFolder Flagged { + get; private set; + } + + /// + /// Gets the special folder containing important messages. + /// + /// The important folder. + public ImapFolder Important { + get; private set; + } + + /// + /// Gets the special folder containing junk messages. + /// + /// The junk folder. + public ImapFolder Junk { + get; private set; + } + + /// + /// Gets the special folder containing sent messages. + /// + /// The sent. + public ImapFolder Sent { + get; private set; + } + + /// + /// Gets the folder containing deleted messages. + /// + /// The trash folder. + public ImapFolder Trash { + get; private set; + } + + #endregion + + internal ImapFolder CreateImapFolder (string encodedName, FolderAttributes attributes, char delim) + { + var args = new ImapFolderConstructorArgs (this, encodedName, attributes, delim); + + return createImapFolder (args); + } + + internal static ImapProtocolException UnexpectedToken (string format, params object[] args) + { + return new ImapProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)) { UnexpectedToken = true }; + } + + internal static void AssertToken (ImapToken token, ImapTokenType type, string format, params object[] args) + { + if (token.Type != type) + throw UnexpectedToken (format, args); + } + + internal static void AssertToken (ImapToken token, ImapTokenType type1, ImapTokenType type2, string format, params object[] args) + { + if (token.Type != type1 && token.Type != type2) + throw UnexpectedToken (format, args); + } + + internal static uint ParseNumber (ImapToken token, bool nonZero, string format, params object[] args) + { + uint value; + + AssertToken (token, ImapTokenType.Atom, format, args); + + if (!uint.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0)) + throw UnexpectedToken (format, args); + + return value; + } + + internal static ulong ParseNumber64 (ImapToken token, bool nonZero, string format, params object[] args) + { + ulong value; + + AssertToken (token, ImapTokenType.Atom, format, args); + + if (!ulong.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0)) + throw UnexpectedToken (format, args); + + return value; + } + + internal static UniqueIdSet ParseUidSet (ImapToken token, uint validity, string format, params object[] args) + { + UniqueIdSet uids; + + AssertToken (token, ImapTokenType.Atom, format, args); + + if (!UniqueIdSet.TryParse ((string) token.Value, validity, out uids)) + throw UnexpectedToken (format, args); + + return uids; + } + + /// + /// Sets the stream - this is only here to be used by the unit tests. + /// + /// The IMAP stream. + internal void SetStream (ImapStream stream) + { + Stream = stream; + } + + /// + /// Takes posession of the and reads the greeting. + /// + /// The IMAP stream. + /// Whether or not asyncrhonois IO methods should be used. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationToken cancellationToken) + { + TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26)); + ProtocolVersion = ImapProtocolVersion.Unknown; + Capabilities = ImapCapabilities.None; + AuthenticationMechanisms.Clear (); + CompressionAlgorithms.Clear (); + ThreadingAlgorithms.Clear (); + SupportedCharsets.Clear (); + SupportedContexts.Clear (); + Rights.Clear (); + + State = ImapEngineState.Connecting; + QuirksMode = ImapQuirksMode.None; + SupportedCharsets.Add ("US-ASCII"); + SupportedCharsets.Add ("UTF-8"); + CapabilitiesVersion = 0; + QResyncEnabled = false; + UTF8Enabled = false; + AppendLimit = null; + Selected = null; + Stream = stream; + I18NLevel = 0; + Tag = 0; + + try { + var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); + + var atom = (string) token.Value; + var text = string.Empty; + var state = State; + var bye = false; + + switch (atom.ToUpperInvariant ()) { + case "BYE": + bye = true; + break; + case "PREAUTH": + state = ImapEngineState.Authenticated; + break; + case "OK": + state = ImapEngineState.Connected; + break; + default: + throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); + if (code.Type == ImapResponseCodeType.Alert) { + OnAlert (code.Message); + + if (bye) + throw new ImapProtocolException (code.Message); + } else { + text = code.Message; + } + } else if (token.Type != ImapTokenType.Eoln) { + text = (string) token.Value; + text += await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + text = text.TrimEnd (); + + if (bye) + throw new ImapProtocolException (text); + } else if (bye) { + throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection."); + } + + if (text.StartsWith ("Courier-IMAP ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Courier; + else if (text.Contains (" Cyrus IMAP ")) + QuirksMode = ImapQuirksMode.Cyrus; + else if (text.StartsWith ("Domino IMAP4 Server", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Domino; + else if (text.StartsWith ("Dovecot ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Dovecot; + else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange; + else if (text.StartsWith ("The Microsoft Exchange IMAP4 service is ready.", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange; + else if (text.StartsWith ("Gimap ready", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.GMail; + else if (text.Contains (" IMAP4rev1 2007f.") || text.Contains (" Panda IMAP ")) + QuirksMode = ImapQuirksMode.UW; + else if (text.Contains ("SmarterMail")) + QuirksMode = ImapQuirksMode.SmarterMail; + else if (text.Contains ("Yandex IMAP4rev1 ")) + QuirksMode = ImapQuirksMode.Yandex; + + State = state; + } catch { + Disconnect (); + throw; + } + } + + /// + /// Disconnects the . + /// + /// + /// Disconnects the . + /// + public void Disconnect () + { + if (Selected != null) { + Selected.Reset (); + Selected.OnClosed (); + Selected = null; + } + + current = null; + + if (Stream != null) { + Stream.Dispose (); + Stream = null; + } + + if (State != ImapEngineState.Disconnected) { + State = ImapEngineState.Disconnected; + OnDisconnected (); + } + } + + internal async Task ReadLineAsync (bool doAsync, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + bool complete; + byte[] buf; + int count; + + do { + if (doAsync) + complete = await Stream.ReadLineAsync (memory, cancellationToken).ConfigureAwait (false); + else + complete = Stream.ReadLine (memory, cancellationToken); + } while (!complete); + + count = (int) memory.Length; +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + buf = memory.GetBuffer (); +#else + buf = memory.ToArray (); +#endif + + try { + return UTF8.GetString (buf, 0, count); + } catch (DecoderFallbackException) { + return Latin1.GetString (buf, 0, count); + } + } + } + +#if false + /// + /// Reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public string ReadLine (CancellationToken cancellationToken) + { + return ReadLineAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task ReadLineAsync (CancellationToken cancellationToken) + { + return ReadLineAsync (true, cancellationToken); + } +#endif + + internal Task ReadTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) + { + return Stream.ReadTokenAsync (specials, doAsync, cancellationToken); + } + + internal Task ReadTokenAsync (bool doAsync, CancellationToken cancellationToken) + { + return Stream.ReadTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken); + } + + internal async Task PeekTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) + { + var token = await ReadTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false); + + Stream.UngetToken (token); + + return token; + } + + internal Task PeekTokenAsync (bool doAsync, CancellationToken cancellationToken) + { + return PeekTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken); + } + + /// + /// Reads the next token. + /// + /// The token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapToken ReadToken (CancellationToken cancellationToken) + { + return Stream.ReadToken (cancellationToken); + } + +#if false + /// + /// Asynchronously reads the next token. + /// + /// The token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task ReadTokenAsync (CancellationToken cancellationToken) + { + return Stream.ReadTokenAsync (cancellationToken); + } + + /// + /// Peeks at the next token. + /// + /// The next token. + /// A list of characters that are not legal in bare string tokens. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapToken PeekToken (string specials, CancellationToken cancellationToken) + { + return PeekTokenAsync (specials, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously peeks at the next token. + /// + /// The next token. + /// A list of characters that are not legal in bare string tokens. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task PeekTokenAsync (string specials, CancellationToken cancellationToken) + { + return PeekTokenAsync (specials, true, cancellationToken); + } + + /// + /// Peeks at the next token. + /// + /// The next token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapToken PeekToken (CancellationToken cancellationToken) + { + return PeekTokenAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously peeks at the next token. + /// + /// The next token. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public Task PeekTokenAsync (CancellationToken cancellationToken) + { + return PeekTokenAsync (true, cancellationToken); + } +#endif + + internal async Task ReadLiteralAsync (bool doAsync, CancellationToken cancellationToken) + { + if (Stream.Mode != ImapStreamMode.Literal) + throw new InvalidOperationException (); + + using (var memory = new MemoryStream (Stream.LiteralLength)) { + var buf = new byte[4096]; + int nread; + + if (doAsync) { + while ((nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0) + memory.Write (buf, 0, nread); + } else { + while ((nread = Stream.Read (buf, 0, buf.Length, cancellationToken)) > 0) + memory.Write (buf, 0, nread); + } + + nread = (int) memory.Length; +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + buf = memory.GetBuffer (); +#else + buf = memory.ToArray (); +#endif + + return Latin1.GetString (buf, 0, nread); + } + } + +#if false + /// + /// Reads the literal as a string. + /// + /// The literal. + /// The cancellation token. + /// + /// The is not in literal mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public string ReadLiteral (CancellationToken cancellationToken) + { + return ReadLiteralAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously reads the literal as a string. + /// + /// The literal. + /// The cancellation token. + /// + /// The is not in literal mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task ReadLiteralAsync (CancellationToken cancellationToken) + { + return ReadLiteralAsync (true, cancellationToken); + } +#endif + + async Task SkipLineAsync (bool doAsync, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Literal) { + var buf = new byte[4096]; + int nread; + + do { + if (doAsync) + nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false); + else + nread = Stream.Read (buf, 0, buf.Length, cancellationToken); + } while (nread > 0); + } + } while (token.Type != ImapTokenType.Eoln); + } + + async Task UpdateCapabilitiesAsync (ImapTokenType sentinel, bool doAsync, CancellationToken cancellationToken) + { + // Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command. + ProtocolVersion = ImapProtocolVersion.Unknown; + Capabilities &= ImapCapabilities.StartTLS; + AuthenticationMechanisms.Clear (); + CompressionAlgorithms.Clear (); + ThreadingAlgorithms.Clear (); + SupportedContexts.Clear (); + CapabilitiesVersion++; + AppendLimit = null; + Rights.Clear (); + I18NLevel = 0; + + var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + if (atom.StartsWith ("AUTH=", StringComparison.OrdinalIgnoreCase)) { + AuthenticationMechanisms.Add (atom.Substring ("AUTH=".Length)); + } else if (atom.StartsWith ("APPENDLIMIT=", StringComparison.OrdinalIgnoreCase)) { + uint limit; + + if (uint.TryParse (atom.Substring ("APPENDLIMIT=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out limit)) + AppendLimit = limit; + + Capabilities |= ImapCapabilities.AppendLimit; + } else if (atom.StartsWith ("COMPRESS=", StringComparison.OrdinalIgnoreCase)) { + CompressionAlgorithms.Add (atom.Substring ("COMPRESS=".Length)); + Capabilities |= ImapCapabilities.Compress; + } else if (atom.StartsWith ("CONTEXT=", StringComparison.OrdinalIgnoreCase)) { + SupportedContexts.Add (atom.Substring ("CONTEXT=".Length)); + Capabilities |= ImapCapabilities.Context; + } else if (atom.StartsWith ("I18NLEVEL=", StringComparison.OrdinalIgnoreCase)) { + int level; + + int.TryParse (atom.Substring ("I18NLEVEL=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out level); + I18NLevel = level; + + Capabilities |= ImapCapabilities.I18NLevel; + } else if (atom.StartsWith ("RIGHTS=", StringComparison.OrdinalIgnoreCase)) { + var rights = atom.Substring ("RIGHTS=".Length); + Rights.AddRange (rights); + } else if (atom.StartsWith ("THREAD=", StringComparison.OrdinalIgnoreCase)) { + var algorithm = atom.Substring ("THREAD=".Length); + switch (algorithm.ToUpperInvariant ()) { + case "ORDEREDSUBJECT": + ThreadingAlgorithms.Add (ThreadingAlgorithm.OrderedSubject); + break; + case "REFERENCES": + ThreadingAlgorithms.Add (ThreadingAlgorithm.References); + break; + } + + Capabilities |= ImapCapabilities.Thread; + } else { + switch (atom.ToUpperInvariant ()) { + case "IMAP4": Capabilities |= ImapCapabilities.IMAP4; break; + case "IMAP4REV1": Capabilities |= ImapCapabilities.IMAP4rev1; break; + case "STATUS": Capabilities |= ImapCapabilities.Status; break; + case "ACL": Capabilities |= ImapCapabilities.Acl; break; + case "QUOTA": Capabilities |= ImapCapabilities.Quota; break; + case "LITERAL+": Capabilities |= ImapCapabilities.LiteralPlus; break; + case "IDLE": Capabilities |= ImapCapabilities.Idle; break; + case "MAILBOX-REFERRALS": Capabilities |= ImapCapabilities.MailboxReferrals; break; + case "LOGIN-REFERRALS": Capabilities |= ImapCapabilities.LoginReferrals; break; + case "NAMESPACE": Capabilities |= ImapCapabilities.Namespace; break; + case "ID": Capabilities |= ImapCapabilities.Id; break; + case "CHILDREN": Capabilities |= ImapCapabilities.Children; break; + case "LOGINDISABLED": Capabilities |= ImapCapabilities.LoginDisabled; break; + case "STARTTLS": Capabilities |= ImapCapabilities.StartTLS; break; + case "MULTIAPPEND": Capabilities |= ImapCapabilities.MultiAppend; break; + case "BINARY": Capabilities |= ImapCapabilities.Binary; break; + case "UNSELECT": Capabilities |= ImapCapabilities.Unselect; break; + case "UIDPLUS": Capabilities |= ImapCapabilities.UidPlus; break; + case "CATENATE": Capabilities |= ImapCapabilities.Catenate; break; + case "CONDSTORE": Capabilities |= ImapCapabilities.CondStore; break; + case "ESEARCH": Capabilities |= ImapCapabilities.ESearch; break; + case "SASL-IR": Capabilities |= ImapCapabilities.SaslIR; break; + case "WITHIN": Capabilities |= ImapCapabilities.Within; break; + case "ENABLE": Capabilities |= ImapCapabilities.Enable; break; + case "QRESYNC": Capabilities |= ImapCapabilities.QuickResync; break; + case "SEARCHRES": Capabilities |= ImapCapabilities.SearchResults; break; + case "SORT": Capabilities |= ImapCapabilities.Sort; break; + case "ANNOTATE-EXPERIMENT-1": Capabilities |= ImapCapabilities.Annotate; break; + case "LIST-EXTENDED": Capabilities |= ImapCapabilities.ListExtended; break; + case "CONVERT": Capabilities |= ImapCapabilities.Convert; break; + case "LANGUAGE": Capabilities |= ImapCapabilities.Language; break; + case "ESORT": Capabilities |= ImapCapabilities.ESort; break; + case "METADATA": Capabilities |= ImapCapabilities.Metadata; break; + case "METADATA-SERVER": Capabilities |= ImapCapabilities.MetadataServer; break; + case "NOTIFY": Capabilities |= ImapCapabilities.Notify; break; + case "LIST-STATUS": Capabilities |= ImapCapabilities.ListStatus; break; + case "SORT=DISPLAY": Capabilities |= ImapCapabilities.SortDisplay; break; + case "CREATE-SPECIAL-USE": Capabilities |= ImapCapabilities.CreateSpecialUse; break; + case "SPECIAL-USE": Capabilities |= ImapCapabilities.SpecialUse; break; + case "SEARCH=FUZZY": Capabilities |= ImapCapabilities.FuzzySearch; break; + case "MULTISEARCH": Capabilities |= ImapCapabilities.MultiSearch; break; + case "MOVE": Capabilities |= ImapCapabilities.Move; break; + case "UTF8=ACCEPT": Capabilities |= ImapCapabilities.UTF8Accept; break; + case "UTF8=ONLY": Capabilities |= ImapCapabilities.UTF8Only; break; + case "LITERAL-": Capabilities |= ImapCapabilities.LiteralMinus; break; + case "APPENDLIMIT": Capabilities |= ImapCapabilities.AppendLimit; break; + case "UNAUTHENTICATE": Capabilities |= ImapCapabilities.Unauthenticate; break; + case "STATUS=SIZE": Capabilities |= ImapCapabilities.StatusSize; break; + case "LIST-MYRIGHTS": Capabilities |= ImapCapabilities.ListMyRights; break; + case "OBJECTID": Capabilities |= ImapCapabilities.ObjectID; break; + case "REPLACE": Capabilities |= ImapCapabilities.Replace; break; + case "XLIST": Capabilities |= ImapCapabilities.XList; break; + case "X-GM-EXT-1": Capabilities |= ImapCapabilities.GMailExt1; QuirksMode = ImapQuirksMode.GMail; break; + case "XSTOP": QuirksMode = ImapQuirksMode.ProtonMail; break; + case "X-SUN-IMAP": QuirksMode = ImapQuirksMode.SunMicrosystems; break; + case "XYMHIGHESTMODSEQ": QuirksMode = ImapQuirksMode.Yahoo; break; + } + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + AssertToken (token, sentinel, GenericItemSyntaxErrorFormat, "CAPABILITIES", token); + + // unget the sentinel + Stream.UngetToken (token); + + if ((Capabilities & ImapCapabilities.IMAP4rev1) != 0) { + ProtocolVersion = ImapProtocolVersion.IMAP4rev1; + Capabilities |= ImapCapabilities.Status; + } else if ((Capabilities & ImapCapabilities.IMAP4) != 0) { + ProtocolVersion = ImapProtocolVersion.IMAP4; + } + + if ((Capabilities & ImapCapabilities.QuickResync) != 0) + Capabilities |= ImapCapabilities.CondStore; + + if ((Capabilities & ImapCapabilities.UTF8Only) != 0) + Capabilities |= ImapCapabilities.UTF8Accept; + } + + async Task UpdateNamespacesAsync (bool doAsync, CancellationToken cancellationToken) + { + var namespaces = new List { + PersonalNamespaces, OtherNamespaces, SharedNamespaces + }; + ImapFolder folder; + ImapToken token; + string path; + char delim; + int n = 0; + + PersonalNamespaces.Clear (); + SharedNamespaces.Clear (); + OtherNamespaces.Clear (); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + do { + if (token.Type == ImapTokenType.OpenParen) { + // parse the list of namespace pairs... + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.OpenParen) { + // parse the namespace pair - first token is the path + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + path = (string) token.Value; + + // second token is the directory separator + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.QString, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + var qstring = token.Type == ImapTokenType.Nil ? string.Empty : (string) token.Value; + + if (qstring.Length > 0) { + delim = qstring[0]; + + // canonicalize the namespace path + path = path.TrimEnd (delim); + } else { + delim = '\0'; + } + + namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path))); + + if (!GetCachedFolder (path, out folder)) { + folder = CreateImapFolder (path, FolderAttributes.None, delim); + CacheFolder (folder); + } + + folder.UpdateIsNamespace (true); + + do { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // NAMESPACE extension + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + + do { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } while (true); + } while (true); + + // read the next token - it should either be '(' or ')' + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } else { + AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + n++; + } while (n < 3); + + while (token.Type != ImapTokenType.Eoln) + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + void ProcessResponseCodes (ImapCommand ic) + { + foreach (var code in ic.RespCodes) { + switch (code.Type) { + case ImapResponseCodeType.Alert: + OnAlert (code.Message); + break; + case ImapResponseCodeType.NotificationOverflow: + OnNotificationOverflow (); + break; + } + } + } + + void EmitMetadataChanged (Metadata metadata) + { + var encodedName = metadata.EncodedName; + ImapFolder folder; + + if (encodedName.Length == 0) { + OnMetadataChanged (metadata); + } else if (FolderCache.TryGetValue (encodedName, out folder)) { + folder.OnMetadataChanged (metadata); + } + } + + internal MetadataCollection FilterMetadata (MetadataCollection metadata, string encodedName) + { + for (int i = 0; i < metadata.Count; i++) { + if (metadata[i].EncodedName == encodedName) + continue; + + EmitMetadataChanged (metadata[i]); + metadata.RemoveAt (i); + i--; + } + + return metadata; + } + + internal void ProcessMetadataChanges (MetadataCollection metadata) + { + for (int i = 0; i < metadata.Count; i++) + EmitMetadataChanged (metadata[i]); + } + + internal static ImapResponseCodeType GetResponseCodeType (string atom) + { + switch (atom.ToUpperInvariant ()) { + case "ALERT": return ImapResponseCodeType.Alert; + case "BADCHARSET": return ImapResponseCodeType.BadCharset; + case "CAPABILITY": return ImapResponseCodeType.Capability; + case "NEWNAME": return ImapResponseCodeType.NewName; + case "PARSE": return ImapResponseCodeType.Parse; + case "PERMANENTFLAGS": return ImapResponseCodeType.PermanentFlags; + case "READ-ONLY": return ImapResponseCodeType.ReadOnly; + case "READ-WRITE": return ImapResponseCodeType.ReadWrite; + case "TRYCREATE": return ImapResponseCodeType.TryCreate; + case "UIDNEXT": return ImapResponseCodeType.UidNext; + case "UIDVALIDITY": return ImapResponseCodeType.UidValidity; + case "UNSEEN": return ImapResponseCodeType.Unseen; + case "REFERRAL": return ImapResponseCodeType.Referral; + case "UNKNOWN-CTE": return ImapResponseCodeType.UnknownCte; + case "APPENDUID": return ImapResponseCodeType.AppendUid; + case "COPYUID": return ImapResponseCodeType.CopyUid; + case "UIDNOTSTICKY": return ImapResponseCodeType.UidNotSticky; + case "URLMECH": return ImapResponseCodeType.UrlMech; + case "BADURL": return ImapResponseCodeType.BadUrl; + case "TOOBIG": return ImapResponseCodeType.TooBig; + case "HIGHESTMODSEQ": return ImapResponseCodeType.HighestModSeq; + case "MODIFIED": return ImapResponseCodeType.Modified; + case "NOMODSEQ": return ImapResponseCodeType.NoModSeq; + case "COMPRESSIONACTIVE": return ImapResponseCodeType.CompressionActive; + case "CLOSED": return ImapResponseCodeType.Closed; + case "NOTSAVED": return ImapResponseCodeType.NotSaved; + case "BADCOMPARATOR": return ImapResponseCodeType.BadComparator; + case "ANNOTATE": return ImapResponseCodeType.Annotate; + case "ANNOTATIONS": return ImapResponseCodeType.Annotations; + case "MAXCONVERTMESSAGES": return ImapResponseCodeType.MaxConvertMessages; + case "MAXCONVERTPARTS": return ImapResponseCodeType.MaxConvertParts; + case "TEMPFAIL": return ImapResponseCodeType.TempFail; + case "NOUPDATE": return ImapResponseCodeType.NoUpdate; + case "METADATA": return ImapResponseCodeType.Metadata; + case "NOTIFICATIONOVERFLOW": return ImapResponseCodeType.NotificationOverflow; + case "BADEVENT": return ImapResponseCodeType.BadEvent; + case "UNDEFINED-FILTER": return ImapResponseCodeType.UndefinedFilter; + case "UNAVAILABLE": return ImapResponseCodeType.Unavailable; + case "AUTHENTICATIONFAILED": return ImapResponseCodeType.AuthenticationFailed; + case "AUTHORIZATIONFAILED": return ImapResponseCodeType.AuthorizationFailed; + case "EXPIRED": return ImapResponseCodeType.Expired; + case "PRIVACYREQUIRED": return ImapResponseCodeType.PrivacyRequired; + case "CONTACTADMIN": return ImapResponseCodeType.ContactAdmin; + case "NOPERM": return ImapResponseCodeType.NoPerm; + case "INUSE": return ImapResponseCodeType.InUse; + case "EXPUNGEISSUED": return ImapResponseCodeType.ExpungeIssued; + case "CORRUPTION": return ImapResponseCodeType.Corruption; + case "SERVERBUG": return ImapResponseCodeType.ServerBug; + case "CLIENTBUG": return ImapResponseCodeType.ClientBug; + case "CANNOT": return ImapResponseCodeType.CanNot; + case "LIMIT": return ImapResponseCodeType.Limit; + case "OVERQUOTA": return ImapResponseCodeType.OverQuota; + case "ALREADYEXISTS": return ImapResponseCodeType.AlreadyExists; + case "NONEXISTENT": return ImapResponseCodeType.NonExistent; + case "USEATTR": return ImapResponseCodeType.UseAttr; + case "MAILBOXID": return ImapResponseCodeType.MailboxId; + default: return ImapResponseCodeType.Unknown; + } + } + + /// + /// Parses the response code. + /// + /// The response code. + /// Whether or not the resp-code is tagged vs untagged. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task ParseResponseCodeAsync (bool isTagged, bool doAsync, CancellationToken cancellationToken) + { + uint validity = Selected != null ? Selected.UidValidity : 0; + ImapResponseCode code; + ImapToken token; + string atom; + +// token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); +// +// if (token.Type != ImapTokenType.LeftBracket) { +// Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token); +// throw UnexpectedToken (token, false); +// } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, "Syntax error in response code. Unexpected token: {0}", token); + + atom = (string) token.Value; + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + code = ImapResponseCode.Create (GetResponseCodeType (atom)); + code.IsTagged = isTagged; + + switch (code.Type) { + case ImapResponseCodeType.BadCharset: + if (token.Type == ImapTokenType.OpenParen) { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + SupportedCharsets.Clear (); + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) { + SupportedCharsets.Add ((string) token.Value); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + break; + case ImapResponseCodeType.Capability: + Stream.UngetToken (token); + await UpdateCapabilitiesAsync (ImapTokenType.CloseBracket, doAsync, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.PermanentFlags: + var perm = (PermanentFlagsResponseCode) code; + + Stream.UngetToken (token); + perm.Flags = await ImapUtils.ParseFlagsListAsync (this, "PERMANENTFLAGS", null, doAsync, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UidNext: + var next = (UidNextResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDNEXT 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDNEXT has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/1010 for an example. + var uid = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token); + next.Uid = uid > 0 ? new UniqueId (uid) : UniqueId.Invalid; + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UidValidity: + var uidvalidity = (UidValidityResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]". + // The *probable* explanation here is that the folder has never been opened and/or no messages + // have ever been delivered (yet) to that mailbox and so the UIDVALIDITY has not (yet) been + // initialized. + // + // See https://github.com/jstedfast/MailKit/issues/150 for an example. + uidvalidity.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Unseen: + var unseen = (UnseenResponseCode) code; + + // Note: we allow '0' here because some servers have been known to send "* OK [UNSEEN 0]" when the + // mailbox contains no messages. + // + // See https://github.com/jstedfast/MailKit/issues/34 for details. + var n = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token); + + unseen.Index = n > 0 ? (int) (n - 1) : 0; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.NewName: + var rename = (NewNameResponseCode) code; + + // Note: this RESP-CODE existed in rfc2060 but has been removed in rfc3501: + // + // 85) Remove NEWNAME. It can't work because mailbox names can be + // literals and can include "]". Functionality can be addressed via + // referrals. + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); + + rename.OldName = (string) token.Value; + + // the next token should be another atom or qstring token representing the new name of the folder + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); + + rename.NewName = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.AppendUid: + var append = (AppendUidResponseCode) code; + + append.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // The MULTIAPPEND extension redefines APPENDUID's second argument to be a uid-set instead of a single uid. + append.UidSet = ParseUidSet (token, append.UidValidity, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.CopyUid: + var copy = (CopyUidResponseCode) code; + + copy.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // Note: Outlook.com will apparently sometimes issue a [COPYUID nz_number SPACE SPACE] resp-code + // in response to a UID COPY or UID MOVE command. Likely this happens only when the source message + // didn't exist or something? See https://github.com/jstedfast/MailKit/issues/555 for details. + + if (token.Type != ImapTokenType.CloseBracket) { + copy.SrcUidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.SrcUidSet = new UniqueIdSet (); + Stream.UngetToken (token); + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type != ImapTokenType.CloseBracket) { + copy.DestUidSet = ParseUidSet (token, copy.UidValidity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); + } else { + copy.DestUidSet = new UniqueIdSet (); + Stream.UngetToken (token); + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.BadUrl: + var badurl = (BadUrlResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "BADURL", token); + + badurl.BadUrl = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.HighestModSeq: + var highest = (HighestModSeqResponseCode) code; + + highest.HighestModSeq = ParseNumber64 (token, false, GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Modified: + var modified = (ModifiedResponseCode) code; + + modified.UidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.MaxConvertMessages: + case ImapResponseCodeType.MaxConvertParts: + var maxConvert = (MaxConvertResponseCode) code; + + maxConvert.MaxConvert = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, atom, token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.NoUpdate: + var noUpdate = (NoUpdateResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token); + + noUpdate.Tag = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Annotate: + var annotate = (AnnotateResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATE", token); + + switch (((string) token.Value).ToUpperInvariant ()) { + case "TOOBIG": + annotate.SubType = AnnotateResponseCodeSubType.TooBig; + break; + case "TOOMANY": + annotate.SubType = AnnotateResponseCodeSubType.TooMany; + break; + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.Annotations: + var annotations = (AnnotationsResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + + switch (((string) token.Value).ToUpperInvariant ()) { + case "NONE": break; + case "READ-ONLY": + annotations.Access = AnnotationAccess.ReadOnly; + break; + default: + annotations.Access = AnnotationAccess.ReadWrite; + annotations.MaxSize = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + break; + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (annotations.Access != AnnotationAccess.None) { + annotations.Scopes = AnnotationScope.Both; + + if (token.Type != ImapTokenType.CloseBracket) { + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); + + if (((string) token.Value).Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) + annotations.Scopes = AnnotationScope.Shared; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + } + + break; + case ImapResponseCodeType.Metadata: + var metadata = (MetadataResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "METADATA", token); + + switch (((string) token.Value).ToUpperInvariant ()) { + case "LONGENTRIES": + metadata.SubType = MetadataResponseCodeSubType.LongEntries; + metadata.IsError = false; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token); + break; + case "MAXSIZE": + metadata.SubType = MetadataResponseCodeSubType.MaxSize; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token); + break; + case "TOOMANY": + metadata.SubType = MetadataResponseCodeSubType.TooMany; + break; + case "NOPRIVATE": + metadata.SubType = MetadataResponseCodeSubType.NoPrivate; + break; + } + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.UndefinedFilter: + var undefined = (UndefinedFilterResponseCode) code; + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token); + + undefined.Name = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapResponseCodeType.MailboxId: + var mailboxid = (MailboxIdResponseCode) code; + + AssertToken (token, ImapTokenType.OpenParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + mailboxid.MailboxId = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + default: + // Note: This code-path handles: [ALERT], [CLOSED], [READ-ONLY], [READ-WRITE], etc. + + //if (code.Type == ImapResponseCodeType.Unknown) + // Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom)); + + // extensions are of the form: "[" atom [SPACE 1*] "]" + + // skip over tokens until we get to a ']' + while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln) + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + break; + } + + AssertToken (token, ImapTokenType.CloseBracket, "Syntax error in response code. Unexpected token: {0}", token); + + code.Message = (await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false)).Trim (); + + return code; + } + + async Task UpdateStatusAsync (bool doAsync, CancellationToken cancellationToken) + { + var token = await ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + ImapFolder folder; + uint count, uid; + ulong modseq; + string name; + + switch (token.Type) { + case ImapTokenType.Literal: + name = await ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + name = (string) token.Value; + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + name = "NIL"; + break; + default: + throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + // Note: if the folder is null, then it probably means the user is using NOTIFY + // and hasn't yet requested the folder. That's ok. + GetCachedFolder (name, out folder); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + do { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + var atom = (string) token.Value; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + switch (atom.ToUpperInvariant ()) { + case "HIGHESTMODSEQ": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + modseq = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateHighestModSeq (modseq); + break; + case "MESSAGES": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.OnExists ((int) count); + break; + case "RECENT": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.OnRecent ((int) count); + break; + case "UIDNEXT": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateUidNext (uid > 0 ? new UniqueId (uid) : UniqueId.Invalid); + break; + case "UIDVALIDITY": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateUidValidity (uid); + break; + case "UNSEEN": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateUnread ((int) count); + break; + case "APPENDLIMIT": + if (token.Type == ImapTokenType.Atom) { + var limit = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateAppendLimit (limit); + } else { + AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + if (folder != null) + folder.UpdateAppendLimit (null); + } + break; + case "SIZE": + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + var size = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateSize (size); + break; + case "MAILBOXID": + AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GenericItemSyntaxErrorFormat, atom, token); + + if (folder != null) + folder.UpdateId ((string) token.Value); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + break; + } + } while (true); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); + } + + /// + /// Processes an untagged response. + /// + /// The untagged response. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + internal async Task ProcessUntaggedResponseAsync (bool doAsync, CancellationToken cancellationToken) + { + var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var folder = current.Folder ?? Selected; + var result = ImapUntaggedResult.Handled; + ImapUntaggedHandler handler; + uint number; + string atom; + + // Note: work around broken IMAP servers such as home.pl which sends "* [COPYUID ...]" resp-codes + // See https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 for details. + if (token.Type == ImapTokenType.OpenBracket) { + // unget the '[' token and then pretend that we got an "OK" + Stream.UngetToken (token); + atom = "OK"; + } else if (token.Type != ImapTokenType.Atom) { + // if we get anything else here, just ignore it? + Stream.UngetToken (token); + await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + return result; + } else { + atom = (string) token.Value; + } + + switch (atom.ToUpperInvariant ()) { + case "BYE": + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); + current.RespCodes.Add (code); + } else { + var text = token.Value.ToString () + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + current.ResponseText = text.TrimEnd (); + } + + current.Bye = true; + + // Note: Yandex IMAP is broken and will continue sending untagged BYE responses until the client closes + // the connection. In order to avoid this scenario, consider this command complete as soon as we receive + // the very first untagged BYE response and do not hold out hoping for a tagged response following the + // untagged BYE. + // + // See https://github.com/jstedfast/MailKit/issues/938 for details. + if (QuirksMode == ImapQuirksMode.Yandex && !current.Logout) + current.Status = ImapCommandStatus.Complete; + break; + case "CAPABILITY": + await UpdateCapabilitiesAsync (ImapTokenType.Eoln, doAsync, cancellationToken); + + // read the eoln token + await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case "ENABLED": + do { + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + break; + + AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + + var feature = (string) token.Value; + switch (feature.ToUpperInvariant ()) { + case "UTF8=ACCEPT": UTF8Enabled = true; break; + case "QRESYNC": QResyncEnabled = true; break; + } + } while (true); + break; + case "FLAGS": + folder.UpdateAcceptedFlags (await ImapUtils.ParseFlagsListAsync (this, atom, null, doAsync, cancellationToken).ConfigureAwait (false)); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, atom, token); + break; + case "NAMESPACE": + await UpdateNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case "STATUS": + await UpdateStatusAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case "OK": case "NO": case "BAD": + if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) + result = ImapUntaggedResult.Ok; + else if (atom.Equals ("NO", StringComparison.OrdinalIgnoreCase)) + result = ImapUntaggedResult.No; + else + result = ImapUntaggedResult.Bad; + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); + current.RespCodes.Add (code); + } else if (token.Type != ImapTokenType.Eoln) { + var text = ((string) token.Value) + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + current.ResponseText = text.TrimEnd (); + } + break; + default: + if (uint.TryParse (atom, NumberStyles.None, CultureInfo.InvariantCulture, out number)) { + // we probably have something like "* 1 EXISTS" + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, "Syntax error in untagged response. Unexpected token: {0}", token); + + atom = (string) token.Value; + + if (current.UntaggedHandlers.TryGetValue (atom, out handler)) { + // the command registered an untagged handler for this atom... + await handler (this, current, (int) number - 1, doAsync).ConfigureAwait (false); + } else if (folder != null) { + switch (atom.ToUpperInvariant ()) { + case "EXISTS": + folder.OnExists ((int) number); + break; + case "EXPUNGE": + if (number == 0) + throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0"); + + folder.OnExpunge ((int) number - 1); + break; + case "FETCH": + // Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes. + // See https://github.com/jstedfast/MailKit/issues/428 for details. + //if (number == 0) + // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); + + await folder.OnFetchAsync (this, (int) number - 1, doAsync, cancellationToken).ConfigureAwait (false); + break; + case "RECENT": + folder.OnRecent ((int) number); + break; + default: + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); + break; + } + } else { + //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); + } + + await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else if (current.UntaggedHandlers.TryGetValue (atom, out handler)) { + // the command registered an untagged handler for this atom... + await handler (this, current, -1, doAsync).ConfigureAwait (false); + await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { + // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event + await ImapUtils.ParseFolderListAsync (this, null, false, true, doAsync, cancellationToken).ConfigureAwait (false); + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token); + } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { + // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange + var metadata = new MetadataCollection (); + await ImapUtils.ParseMetadataAsync (this, metadata, doAsync, cancellationToken).ConfigureAwait (false); + ProcessMetadataChanges (metadata); + + token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token); + } else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) { + await folder.OnVanishedAsync (this, doAsync, cancellationToken).ConfigureAwait (false); + await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else { + // don't know how to handle this... eat it? + await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + break; + } + + return result; + } + + /// + /// Iterate the command pipeline. + /// + async Task IterateAsync (bool doAsync) + { + lock (queue) { + if (queue.Count == 0) + throw new InvalidOperationException ("The IMAP command queue is empty."); + + if (IsBusy) + throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads."); + + current = queue[0]; + queue.RemoveAt (0); + + try { + current.CancellationToken.ThrowIfCancellationRequested (); + } catch { + queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); + current = null; + throw; + } + } + + current.Status = ImapCommandStatus.Active; + + try { + while (await current.StepAsync (doAsync).ConfigureAwait (false)) { + // more literal data to send... + } + + if (current.Bye && !current.Logout) + throw new ImapProtocolException ("Bye."); + } catch (ImapProtocolException) { + var ic = current; + + Disconnect (); + + if (ic.Bye) { + if (ic.RespCodes.Count > 0) { + var code = ic.RespCodes[ic.RespCodes.Count - 1]; + + if (code.Type == ImapResponseCodeType.Alert) { + OnAlert (code.Message); + + throw new ImapProtocolException (code.Message); + } + } + + if (!string.IsNullOrEmpty (ic.ResponseText)) + throw new ImapProtocolException (ic.ResponseText); + } + + throw; + } catch { + Disconnect (); + throw; + } finally { + current = null; + } + } + + /// + /// Wait for the specified command to finish. + /// + /// The IMAP command. + /// Whether or not asynchronous IO methods should be used. + /// + /// is null. + /// + public async Task RunAsync (ImapCommand ic, bool doAsync) + { + if (ic == null) + throw new ArgumentNullException (nameof (ic)); + + while (ic.Status < ImapCommandStatus.Complete) { + // continue processing commands... + await IterateAsync (doAsync).ConfigureAwait (false); + } + + ProcessResponseCodes (ic); + } + + public IEnumerable CreateCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) + { + var vargs = new List (); + int maxLength; + + // we assume that uids is the first formatter (with a %s) + vargs.Add ("1"); + + for (int i = 0; i < args.Length; i++) + vargs.Add (args[i]); + + args = vargs.ToArray (); + + if (QuirksMode == ImapQuirksMode.Courier) { + // Courier IMAP's command parser allows each token to be up to 16k in size. + maxLength = 16 * 1024; + } else { + int estimated = ImapCommand.EstimateCommandLength (this, format, args); + + switch (QuirksMode) { + case ImapQuirksMode.Dovecot: + // Dovecot, by default, allows commands up to 64k. + // See https://github.com/dovecot/core/blob/master/src/imap/imap-settings.c#L94 + maxLength = Math.Max ((64 * 1042) - estimated, 24); + break; + case ImapQuirksMode.GMail: + // GMail seems to support command-lines up to at least 16k. + maxLength = Math.Max ((16 * 1042) - estimated, 24); + break; + case ImapQuirksMode.Yahoo: + case ImapQuirksMode.UW: + // Follow the IMAP4 Implementation Recommendations which states that clients + // *SHOULD* limit their command lengths to 1000 octets. + maxLength = Math.Max (1000 - estimated, 24); + break; + default: + // Push the boundaries of the IMAP4 Implementation Recommendations which states + // that servers *SHOULD* accept command lengths of up to 8000 octets. + maxLength = Math.Max (8000 - estimated, 24); + break; + } + } + + foreach (var subset in UniqueIdSet.EnumerateSerializedSubsets (uids, maxLength)) { + args[0] = subset; + + yield return new ImapCommand (this, cancellationToken, folder, format, args); + } + } + + public IEnumerable QueueCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) + { + foreach (var ic in CreateCommands (cancellationToken, folder, format, uids, args)) { + QueueCommand (ic); + yield return ic; + } + } + + /// + /// Queues the command. + /// + /// The command. + /// The cancellation token. + /// The folder that the command operates on. + /// The formatting options. + /// The command format. + /// The command arguments. + public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) + { + var ic = new ImapCommand (this, cancellationToken, folder, options, format, args); + QueueCommand (ic); + return ic; + } + + /// + /// Queues the command. + /// + /// The command. + /// The cancellation token. + /// The folder that the command operates on. + /// The command format. + /// The command arguments. + public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) + { + return QueueCommand (cancellationToken, folder, FormatOptions.Default, format, args); + } + + /// + /// Queues the command. + /// + /// The IMAP command. + public void QueueCommand (ImapCommand ic) + { + lock (queue) { + ic.Status = ImapCommandStatus.Queued; + queue.Add (ic); + } + } + + /// + /// Queries the capabilities. + /// + /// The command result. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task QueryCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken) + { + var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + return ic.Response; + } + + /// + /// Cache the specified folder. + /// + /// The folder. + public void CacheFolder (ImapFolder folder) + { + if ((folder.Attributes & FolderAttributes.Inbox) != 0) + cacheComparer.DirectorySeparator = folder.DirectorySeparator; + + FolderCache.Add (folder.EncodedName, folder); + } + + /// + /// Gets the cached folder. + /// + /// true if the folder was retreived from the cache; otherwise, false. + /// The encoded folder name. + /// The cached folder. + public bool GetCachedFolder (string encodedName, out ImapFolder folder) + { + return FolderCache.TryGetValue (encodedName, out folder); + } + + /// + /// Looks up and sets the property of each of the folders. + /// + /// The IMAP folders. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + internal async Task LookupParentFoldersAsync (IEnumerable folders, bool doAsync, CancellationToken cancellationToken) + { + var list = new List (folders); + string encodedName, pattern; + ImapFolder parent; + int index; + + // Note: we use a for-loop instead of foreach because we conditionally add items to the list. + for (int i = 0; i < list.Count; i++) { + var folder = list[i]; + + if (folder.ParentFolder != null) + continue; + + // FIXME: should this search EncodedName instead of FullName? + if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { + if (index == 0) + continue; + + var parentName = folder.FullName.Substring (0, index); + encodedName = EncodeMailboxName (parentName); + } else { + encodedName = string.Empty; + } + + if (GetCachedFolder (encodedName, out parent)) { + folder.ParentFolder = parent; + continue; + } + + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + pattern = encodedName.Replace ('*', '%'); + + var command = new StringBuilder ("LIST \"\" %S"); + var returnsSubscribed = false; + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + // Try to get the \Subscribed and \HasChildren or \HasNoChildren attributes + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = new List (); + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + if (!GetCachedFolder (encodedName, out parent)) { + parent = CreateImapFolder (encodedName, FolderAttributes.NonExistent, folder.DirectorySeparator); + CacheFolder (parent); + } else if (parent.ParentFolder == null && !parent.IsNamespace) { + list.Add (parent); + } + + folder.ParentFolder = parent; + } + } + + /// + /// Queries the namespaces. + /// + /// The command result. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task QueryNamespacesAsync (bool doAsync, CancellationToken cancellationToken) + { + ImapCommand ic; + + if ((Capabilities & ImapCapabilities.Namespace) != 0) { + ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); + await RunAsync (ic, doAsync).ConfigureAwait (false); + } else { + var list = new List (); + + ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list; + + QueueCommand (ic); + await RunAsync (ic, doAsync).ConfigureAwait (false); + + PersonalNamespaces.Clear (); + SharedNamespaces.Clear (); + OtherNamespaces.Clear (); + + if (list.Count > 0) { + var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0); + + if (empty == null) { + empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator); + CacheFolder (empty); + } + + PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName)); + empty.UpdateIsNamespace (true); + } + + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + } + + return ic.Response; + } + + internal static ImapFolder GetFolder (List folders, string encodedName) + { + for (int i = 0; i < folders.Count; i++) { + if (encodedName.Equals (folders[i].EncodedName, StringComparison.OrdinalIgnoreCase)) + return folders[i]; + } + + return null; + } + + /// + /// Assigns a folder as a special folder. + /// + /// The special folder. + public void AssignSpecialFolder (ImapFolder folder) + { + if ((folder.Attributes & FolderAttributes.All) != 0) + All = folder; + if ((folder.Attributes & FolderAttributes.Archive) != 0) + Archive = folder; + if ((folder.Attributes & FolderAttributes.Drafts) != 0) + Drafts = folder; + if ((folder.Attributes & FolderAttributes.Flagged) != 0) + Flagged = folder; + if ((folder.Attributes & FolderAttributes.Important) != 0) + Important = folder; + if ((folder.Attributes & FolderAttributes.Junk) != 0) + Junk = folder; + if ((folder.Attributes & FolderAttributes.Sent) != 0) + Sent = folder; + if ((folder.Attributes & FolderAttributes.Trash) != 0) + Trash = folder; + } + + /// + /// Assigns the special folders. + /// + /// The list of folders. + public void AssignSpecialFolders (IList list) + { + for (int i = 0; i < list.Count; i++) + AssignSpecialFolder (list[i]); + } + + /// + /// Queries the special folders. + /// + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task QuerySpecialFoldersAsync (bool doAsync, CancellationToken cancellationToken) + { + var command = new StringBuilder ("LIST \"\" \"INBOX\""); + var list = new List (); + var returnsSubscribed = false; + ImapFolder folder; + ImapCommand ic; + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + GetCachedFolder ("INBOX", out folder); + Inbox = folder; + + list.Clear (); + + if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { + // Note: Some IMAP servers like ProtonMail respond to SPECIAL-USE LIST queries with BAD, so fall + // back to just issuing a standard LIST command and hope we get back some SPECIAL-USE attributes. + // + // See https://github.com/jstedfast/MailKit/issues/674 for dertails. + returnsSubscribed = false; + command.Clear (); + + command.Append ("LIST "); + + if (QuirksMode != ImapQuirksMode.ProtonMail) + command.Append ("(SPECIAL-USE) \"\" \"*\""); + else + command.Append ("\"\" \"%%\""); + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + AssignSpecialFolders (list); + } else if ((Capabilities & ImapCapabilities.XList) != 0) { + ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); + ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + AssignSpecialFolders (list); + } + } + + /// + /// Gets the folder representing the specified quota root. + /// + /// The folder. + /// The name of the quota root. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task GetQuotaRootFolderAsync (string quotaRoot, bool doAsync, CancellationToken cancellationToken) + { + ImapFolder folder; + + if (GetCachedFolder (quotaRoot, out folder)) + return folder; + + var command = new StringBuilder ("LIST \"\" %S"); + var list = new List (); + var returnsSubscribed = false; + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), quotaRoot); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("LIST", ic); + + if ((folder = GetFolder (list, quotaRoot)) == null) { + folder = CreateImapFolder (quotaRoot, FolderAttributes.NonExistent, '.'); + CacheFolder (folder); + return folder; + } + + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + return folder; + } + + /// + /// Gets the folder for the specified path. + /// + /// The folder. + /// The folder path. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task GetFolderAsync (string path, bool doAsync, CancellationToken cancellationToken) + { + var encodedName = EncodeMailboxName (path); + ImapFolder folder; + + if (GetCachedFolder (encodedName, out folder)) + return folder; + + var command = new StringBuilder ("LIST \"\" %S"); + var list = new List (); + var returnsSubscribed = false; + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), encodedName); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("LIST", ic); + + if ((folder = GetFolder (list, encodedName)) == null) + throw new FolderNotFoundException (path); + + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + return folder; + } + + internal string GetStatusQuery (StatusItems items) + { + var flags = string.Empty; + + if ((items & StatusItems.Count) != 0) + flags += "MESSAGES "; + if ((items & StatusItems.Recent) != 0) + flags += "RECENT "; + if ((items & StatusItems.UidNext) != 0) + flags += "UIDNEXT "; + if ((items & StatusItems.UidValidity) != 0) + flags += "UIDVALIDITY "; + if ((items & StatusItems.Unread) != 0) + flags += "UNSEEN "; + + if ((Capabilities & ImapCapabilities.CondStore) != 0) { + if ((items & StatusItems.HighestModSeq) != 0) + flags += "HIGHESTMODSEQ "; + } + + // Note: If the IMAP server specifies a limit in the CAPABILITY response, then + // it seems we cannot expect to be able to query this in a STATUS command... + if ((Capabilities & ImapCapabilities.AppendLimit) != 0 && !AppendLimit.HasValue) { + if ((items & StatusItems.AppendLimit) != 0) + flags += "APPENDLIMIT "; + } + + if ((Capabilities & ImapCapabilities.StatusSize) != 0) { + if ((items & StatusItems.Size) != 0) + flags += "SIZE "; + } + + if ((Capabilities & ImapCapabilities.ObjectID) != 0) { + if ((items & StatusItems.MailboxId) != 0) + flags += "MAILBOXID "; + } + + return flags.TrimEnd (); + } + + /// + /// Get all of the folders within the specified namespace. + /// + /// + /// Gets all of the folders within the specified namespace. + /// + /// The list of folders. + /// The namespace. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken) + { + var encodedName = EncodeMailboxName (@namespace.Path); + var pattern = encodedName.Length > 0 ? encodedName + @namespace.DirectorySeparator : string.Empty; + var status = items != StatusItems.None; + var list = new List (); + var command = new StringBuilder (); + var returnsSubscribed = false; + var lsub = subscribedOnly; + ImapFolder folder; + + if (!GetCachedFolder (encodedName, out folder)) + throw new FolderNotFoundException (@namespace.Path); + + if (subscribedOnly) { + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append ("LIST (SUBSCRIBED)"); + returnsSubscribed = true; + lsub = false; + } else { + command.Append ("LSUB"); + } + } else { + command.Append ("LIST"); + } + + command.Append (" \"\" %S"); + + if (!lsub) { + if (items != StatusItems.None && (Capabilities & ImapCapabilities.ListStatus) != 0) { + command.Append (" RETURN ("); + + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN "); + } + + command.AppendFormat ("STATUS ({0})", GetStatusQuery (items)); + command.Append (')'); + status = false; + } else if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN ("); + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN"); + command.Append (')'); + } + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern + "*"); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + ic.Lsub = lsub; + + QueueCommand (ic); + + await RunAsync (ic, doAsync).ConfigureAwait (false); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic); + + await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + if (status) { + for (int i = 0; i < list.Count; i++) { + if (list[i].Exists) + await list[i].StatusAsync (items, doAsync, false, cancellationToken).ConfigureAwait (false); + } + } + + return list; + } + + /// + /// Decodes the name of the mailbox. + /// + /// The mailbox name. + /// The encoded name. + public string DecodeMailboxName (string encodedName) + { + return UTF8Enabled ? encodedName : ImapEncoding.Decode (encodedName); + } + + /// + /// Encodes the name of the mailbox. + /// + /// The mailbox name. + /// The encoded mailbox name. + public string EncodeMailboxName (string mailboxName) + { + return UTF8Enabled ? mailboxName : ImapEncoding.Encode (mailboxName); + } + + /// + /// Determines whether the mailbox name is valid or not. + /// + /// true if the mailbox name is valid; otherwise, false. + /// The mailbox name. + /// The path delimeter. + public bool IsValidMailboxName (string mailboxName, char delim) + { + // From rfc6855: + // + // Mailbox names MUST comply with the Net-Unicode Definition ([RFC5198], Section 2) + // with the specific exception that they MUST NOT contain control characters + // (U+0000-U+001F and U+0080-U+009F), a delete character (U+007F), a line separator (U+2028), + // or a paragraph separator (U+2029). + for (int i = 0; i < mailboxName.Length; i++) { + char c = mailboxName[i]; + + if (c <= 0x1F || (c >= 0x80 && c <= 0x9F) || c == 0x7F || c == 0x2028 || c == 0x2029 || c == delim) + return false; + } + + return mailboxName.Length > 0; + } + + void InitializeParser (Stream stream, bool persistent) + { + if (parser == null) + parser = new MimeParser (ParserOptions.Default, stream, persistent); + else + parser.SetStream (ParserOptions.Default, stream, persistent); + } + + public async Task ParseHeadersAsync (Stream stream, bool doAsync, CancellationToken cancellationToken) + { + InitializeParser (stream, false); + + if (doAsync) + return await parser.ParseHeadersAsync (cancellationToken).ConfigureAwait (false); + + return parser.ParseHeaders (cancellationToken); + } + + public async Task ParseMessageAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken) + { + InitializeParser (stream, persistent); + + if (doAsync) + return await parser.ParseMessageAsync (cancellationToken).ConfigureAwait (false); + + return parser.ParseMessage (cancellationToken); + } + + public async Task ParseEntityAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken) + { + InitializeParser (stream, persistent); + + if (doAsync) + return await parser.ParseEntityAsync (cancellationToken).ConfigureAwait (false); + + return parser.ParseEntity (cancellationToken); + } + + /// + /// Occurs when the engine receives an alert message from the server. + /// + public event EventHandler Alert; + + internal void OnAlert (string message) + { + var handler = Alert; + + if (handler != null) + handler (this, new AlertEventArgs (message)); + } + + /// + /// Occurs when the engine receives a notification that a folder has been created. + /// + public event EventHandler FolderCreated; + + internal void OnFolderCreated (IMailFolder folder) + { + var handler = FolderCreated; + + if (handler != null) + handler (this, new FolderCreatedEventArgs (folder)); + } + + /// + /// Occurs when the engine receives a notification that metadata has changed. + /// + public event EventHandler MetadataChanged; + + internal void OnMetadataChanged (Metadata metadata) + { + var handler = MetadataChanged; + + if (handler != null) + handler (this, new MetadataChangedEventArgs (metadata)); + } + + /// + /// Occurs when the engine receives a notification overflow message from the server. + /// + public event EventHandler NotificationOverflow; + + internal void OnNotificationOverflow () + { + // [NOTIFICATIONOVERFLOW] will reset to NOTIFY NONE + NotifySelectedNewExpunge = false; + + var handler = NotificationOverflow; + + if (handler != null) + handler (this, EventArgs.Empty); + } + + public event EventHandler Disconnected; + + void OnDisconnected () + { + var handler = Disconnected; + + if (handler != null) + handler (this, EventArgs.Empty); + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + disposed = true; + Disconnect (); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapEventGroup.cs b/src/MailKit/Net/Imap/ImapEventGroup.cs new file mode 100644 index 0000000..e60255e --- /dev/null +++ b/src/MailKit/Net/Imap/ImapEventGroup.cs @@ -0,0 +1,692 @@ +// +// ImapFolderFetch.cs +// +// Authors: Steffen Kieß +// Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit; + +namespace MailKit.Net.Imap { + /// + /// An IMAP event group used with the NOTIFY command. + /// + /// + /// An IMAP event group used with the NOTIFY command. + /// + public sealed class ImapEventGroup + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The mailbox filter. + /// The list of IMAP events. + /// + /// is null. + /// -or- + /// is null. + /// + public ImapEventGroup (ImapMailboxFilter mailboxFilter, IList events) + { + if (mailboxFilter == null) + throw new ArgumentNullException (nameof (mailboxFilter)); + + if (events == null) + throw new ArgumentNullException (nameof (events)); + + MailboxFilter = mailboxFilter; + Events = events; + } + + /// + /// Get the mailbox filter. + /// + /// + /// Gets the mailbox filter. + /// + /// The mailbox filter. + public ImapMailboxFilter MailboxFilter { + get; private set; + } + + /// + /// Get the list of IMAP events. + /// + /// + /// Gets the list of IMAP events. + /// + /// The events. + public IList Events { + get; private set; + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP event group. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP event group. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// Gets set to true if the NOTIFY command requests the MessageNew or + /// MessageExpunged events for a SELECTED or SELECTED-DELAYED mailbox filter; otherwise it is left unchanged. + internal void Format (ImapEngine engine, StringBuilder command, IList args, ref bool notifySelectedNewExpunge) + { + bool isSelectedFilter = MailboxFilter == ImapMailboxFilter.Selected || MailboxFilter == ImapMailboxFilter.SelectedDelayed; + + command.Append ("("); + MailboxFilter.Format (engine, command, args); + command.Append (" "); + + if (Events.Count > 0) { + var haveAnnotationChange = false; + var haveMessageExpunge = false; + var haveMessageNew = false; + var haveFlagChange = false; + + command.Append ("("); + + for (int i = 0; i < Events.Count; i++) { + var @event = Events[i]; + + if (isSelectedFilter && !@event.IsMessageEvent) + throw new InvalidOperationException ("Only message events may be specified when SELECTED or SELECTED-DELAYED is used."); + + if (@event is ImapEvent.MessageNew) + haveMessageNew = true; + else if (@event == ImapEvent.MessageExpunge) + haveMessageExpunge = true; + else if (@event == ImapEvent.FlagChange) + haveFlagChange = true; + else if (@event == ImapEvent.AnnotationChange) + haveAnnotationChange = true; + + if (i > 0) + command.Append (" "); + + @event.Format (engine, command, args, isSelectedFilter); + } + command.Append (")"); + + // https://tools.ietf.org/html/rfc5465#section-5 + if ((haveMessageNew && !haveMessageExpunge) || (!haveMessageNew && haveMessageExpunge)) + throw new InvalidOperationException ("If MessageNew or MessageExpunge is specified, both must be specified."); + + if ((haveFlagChange || haveAnnotationChange) && (!haveMessageNew || !haveMessageExpunge)) + throw new InvalidOperationException ("If FlagChange and/or AnnotationChange are specified, MessageNew and MessageExpunge must also be specified."); + + notifySelectedNewExpunge = (haveMessageNew || haveMessageExpunge) && MailboxFilter == ImapMailboxFilter.Selected; + } else { + command.Append ("NONE"); + } + + command.Append (")"); + } + } + + /// + /// An IMAP mailbox filter for use with the NOTIFY command. + /// + /// + /// An IMAP mailbox filter for use with the NOTIFY command. + /// + public class ImapMailboxFilter + { + /// + /// An IMAP mailbox filter specifying that the client wants immediate notifications for + /// the currently selected folder. + /// + /// + /// The SELECTED mailbox specifier requires the server to send immediate + /// notifications for the currently selected mailbox about all specified + /// message events. + /// + public static readonly ImapMailboxFilter Selected = new ImapMailboxFilter ("SELECTED"); + + /// + /// An IMAP mailbox filter specifying the currently selected folder but delays notifications + /// until a command has been issued. + /// + /// + /// The SELECTED-DELAYED mailbox specifier requires the server to delay a + /// event until the client issues a command that allows + /// returning information about expunged messages (see + /// Section 7.4.1 of RFC3501] + /// for more details), for example, till a NOOP or an IDLE command has been issued. + /// When SELECTED-DELAYED is specified, the server MAY also delay returning other message + /// events until the client issues one of the commands specified above, or it MAY return them + /// immediately. + /// + public static readonly ImapMailboxFilter SelectedDelayed = new ImapMailboxFilter ("SELECTED-DELAYED"); + + /// + /// An IMAP mailbox filter specifying the currently selected folder. + /// + /// + /// The INBOXES mailbox specifier refers to all selectable mailboxes in the user's + /// personal namespace(s) to which messages may be delivered by a Message Delivery Agent (MDA). + /// + /// If the IMAP server cannot easily compute this set, it MUST treat + /// as equivalent to . + /// + public static readonly ImapMailboxFilter Inboxes = new ImapMailboxFilter ("INBOXES"); + + /// + /// An IMAP mailbox filter specifying all selectable folders within the user's personal namespace. + /// + /// + /// The PERSONAL mailbox specifier refers to all selectable folders within the user's personal namespace. + /// + public static readonly ImapMailboxFilter Personal = new ImapMailboxFilter ("PERSONAL"); + + /// + /// An IMAP mailbox filter that refers to all subscribed folders. + /// + /// + /// The SUBSCRIBED mailbox specifier refers to all folders subscribed to by the user. + /// If the subscription list changes, the server MUST reevaluate the list. + /// + public static readonly ImapMailboxFilter Subscribed = new ImapMailboxFilter ("SUBSCRIBED"); + + /// + /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. + /// + /// + /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. + /// + public class Mailboxes : ImapMailboxFilter + { + readonly ImapFolder[] folders; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is null. + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Mailboxes (IList folders) : this ("MAILBOXES", folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is null. + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Mailboxes (params IMailFolder[] folders) : this ("MAILBOXES", folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the mailbox filter. + /// The list of folders to watch for events. + /// + /// is null. + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + internal Mailboxes (string name, IList folders) : base (name) + { + if (folders == null) + throw new ArgumentNullException (nameof (folders)); + + if (folders.Count == 0) + throw new ArgumentException ("Must supply at least one folder.", nameof (folders)); + + this.folders = new ImapFolder[folders.Count]; + for (int i = 0; i < folders.Count; i++) { + if (!(folders[i] is ImapFolder folder)) + throw new ArgumentException ("All folders must be ImapFolders.", nameof (folders)); + + this.folders[i] = folder; + } + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + internal override void Format (ImapEngine engine, StringBuilder command, IList args) + { + command.Append (Name); + command.Append (' '); + + // FIXME: should we verify that each ImapFolder belongs to this ImapEngine? + + if (folders.Length == 1) { + command.Append ("%F"); + args.Add (folders[0]); + } else { + command.Append ("("); + + for (int i = 0; i < folders.Length; i++) { + if (i > 0) + command.Append (" "); + command.Append ("%F"); + args.Add (folders[i]); + } + + command.Append (")"); + } + } + } + + /// + /// An IMAP mailbox filter that specifies a list of folder subtrees to get notifications about. + /// + /// + /// The client will receive notifications for each specified folder plus all selectable + /// folders that are subordinate to any of the specified folders. + /// + public class Subtree : Mailboxes + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is null. + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Subtree (IList folders) : base ("SUBTREE", folders) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The list of folders to watch for events. + /// + /// is null. + /// + /// + /// The list of is empty. + /// -or- + /// The list of contains folders that are not of + /// type . + /// + public Subtree (params IMailFolder[] folders) : base ("SUBTREE", folders) + { + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the mailbox filter. + internal ImapMailboxFilter (string name) + { + Name = name; + } + + /// + /// Get the name of the mailbox filter. + /// + /// + /// Gets the name of the mailbox filter. + /// + /// The name. + public string Name { get; private set; } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + internal virtual void Format (ImapEngine engine, StringBuilder command, IList args) + { + command.Append (Name); + } + } + + /// + /// An IMAP notification event. + /// + /// + /// An IMAP notification event. + /// + public class ImapEvent + { + /// + /// An IMAP event notification for expunged messages. + /// + /// + /// If the expunged message or messages are in the selected mailbox, the server notifies the client + /// using (or if + /// the QRESYNC extension has been enabled via + /// or + /// ). + /// If the expunged message or messages are in another mailbox, the + /// and properties will be updated and the appropriate + /// and events will be + /// emitted for the relevant folder. If the QRESYNC + /// extension is enabled, the property will also be updated and + /// the event will be emitted. + /// if a client requests with the + /// mailbox specifier, the meaning of a message index can change at any time, so the client cannot use + /// message indexes in commands anymore. The client MUST use API variants that take or + /// a . The meaning of ** can also change when messages are added or expunged. + /// A client wishing to keep using message indexes can either use the + /// mailbox specifier or can avoid using the event entirely. + /// + public static readonly ImapEvent MessageExpunge = new ImapEvent ("MessageExpunge", true); + + /// + /// An IMAP event notification for message flag changes. + /// + /// + /// If the notification arrives for a message located in the currently selected + /// folder, then that folder will emit a event as well as a + /// event with an appropriately populated + /// . + /// On the other hand, if the notification arrives for a message that is not + /// located in the currently selected folder, then the events that are emitted will depend on the + /// of the IMAP server. + /// If the server supports the capability (or the + /// capability and the client has enabled it via + /// ), then the + /// event will be emitted as well as the + /// event (if the latter has changed). If the number of + /// seen messages has changed, then the event may also be emitted. + /// If the server does not support either the capability nor + /// the capability and the client has not enabled the later capability + /// via , then the server may choose + /// only to notify the client of changes by emitting the + /// event. + /// + public static readonly ImapEvent FlagChange = new ImapEvent ("FlagChange", true); + + /// + /// An IMAP event notification for message annotation changes. + /// + /// + /// If the notification arrives for a message located in the currently selected + /// folder, then that folder will emit a event as well as a + /// event with an appropriately populated + /// . + /// On the other hand, if the notification arrives for a message that is not + /// located in the currently selected folder, then the events that are emitted will depend on the + /// of the IMAP server. + /// If the server supports the capability (or the + /// capability and the client has enabled it via + /// ), then the + /// event will be emitted as well as the + /// event (if the latter has changed). If the number of + /// seen messages has changed, then the event may also be emitted. + /// If the server does not support either the capability nor + /// the capability and the client has not enabled the later capability + /// via , then the server may choose + /// only to notify the client of changes by emitting the + /// event. + /// + public static readonly ImapEvent AnnotationChange = new ImapEvent ("AnnotationChange", true); + + /// + /// AN IMAP event notification for folders that have been created, deleted, or renamed. + /// + /// + /// These notifications are sent if an affected mailbox name was created, deleted, or renamed. + /// As these notifications are received by the client, the apropriate will be emitted: + /// , , or + /// , respectively. + /// If the server supports , granting or revocation of the + /// right to the current user on the affected folder will also be + /// considered folder creation or deletion, respectively. If a folder is created or deleted, the folder itself + /// and its direct parent (whether it is an existing folder or not) are considered to be affected. + /// + public static readonly ImapEvent MailboxName = new ImapEvent ("MailboxName", false); + + /// + /// An IMAP event notification for folders who have had their subscription status changed. + /// + /// + /// This event requests that the server notifies the client of any subscription changes, + /// causing the or + /// events to be emitted accordingly on the affected . + /// + public static readonly ImapEvent SubscriptionChange = new ImapEvent ("SubscriptionChange", false); + + /// + /// An IMAP event notification for changes to folder metadata. + /// + /// + /// Support for this event type is OPTIONAL unless is supported + /// by the server, in which case support for this event type is REQUIRED. + /// If the server does support this event, then the event + /// will be emitted whenever metadata changes for any folder included in the . + /// + public static readonly ImapEvent MailboxMetadataChange = new ImapEvent ("MailboxMetadataChange", false); + + /// + /// An IMAP event notification for changes to server metadata. + /// + /// + /// Support for this event type is OPTIONAL unless is supported + /// by the server, in which case support for this event type is REQUIRED. + /// If the server does support this event, then the event + /// will be emitted whenever metadata changes. + /// + public static readonly ImapEvent ServerMetadataChange = new ImapEvent ("ServerMetadataChange", false); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the IMAP event. + /// true if the event is a message event; otherwise, false. + internal ImapEvent (string name, bool isMessageEvent) + { + IsMessageEvent = isMessageEvent; + Name = name; + } + + /// + /// Get whether or not this is a message event. + /// + /// + /// Gets whether or not this is a message event. + /// + /// true if is message event; otherwise, false. + internal bool IsMessageEvent { + get; private set; + } + + /// + /// Get the name of the IMAP event. + /// + /// + /// Gets the name of the IMAP event. + /// + /// The name of the IMAP event. + public string Name { + get; private set; + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// true if the event is being registered for a + /// or + /// mailbox filter. + internal virtual void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) + { + command.Append (Name); + } + + /// + /// An IMAP event notification for new or appended messages. + /// + /// + /// An IMAP event notification for new or appended messages. + /// If the new or appended message is in the selected folder, the folder will emit the + /// event, followed by a + /// event containing the information requested by the client. + /// These events will not be emitted for any message created by the client on this particular folder + /// as a result of, for example, a call to + /// + /// or . + /// + public class MessageNew : ImapEvent + { + readonly MessageSummaryItems items; + readonly HashSet headers; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + public MessageNew (MessageSummaryItems items = MessageSummaryItems.None) : base ("MessageNew", true) + { + headers = ImapFolder.EmptyHeaderFields; + this.items = items; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + /// Additional message headers to retrieve for new messages. + public MessageNew (MessageSummaryItems items, HashSet headers) : this (items) + { + this.headers = ImapUtils.GetUniqueHeaders (headers); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The message summary items to automatically retrieve for new messages. + /// Additional message headers to retrieve for new messages. + public MessageNew (MessageSummaryItems items, HashSet headers) : this (items) + { + this.headers = ImapUtils.GetUniqueHeaders (headers); + } + + /// + /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// + /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. + /// + /// The IMAP engine. + /// The IMAP command builder. + /// The IMAP command argument builder. + /// true if the event is being registered for a + /// or + /// mailbox filter. + internal override void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) + { + command.Append (Name); + + if (items == MessageSummaryItems.None && headers.Count == 0) + return; + + if (!isSelectedFilter) + throw new InvalidOperationException ("The MessageNew event cannot have any parameters for mailbox filters other than SELECTED and SELECTED-DELAYED."); + + var xitems = items; + bool previewText; + + command.Append (" "); + command.Append (ImapFolder.FormatSummaryItems (engine, ref xitems, headers, out previewText, isNotify: true)); + } + } + } +} diff --git a/src/MailKit/Net/Imap/ImapFolder.cs b/src/MailKit/Net/Imap/ImapFolder.cs new file mode 100644 index 0000000..8c4ae88 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolder.cs @@ -0,0 +1,6224 @@ +// +// ImapFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Search; + +namespace MailKit.Net.Imap { + /// + /// An IMAP folder. + /// + /// + /// An IMAP folder. + /// + /// + /// + /// + /// + /// + /// + public partial class ImapFolder : MailFolder, IImapFolder + { + bool supportsModSeq; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// If you subclass , you will also need to subclass + /// and override the + /// + /// method in order to return a new instance of your ImapFolder subclass. + /// + /// The constructor arguments. + /// + /// is null. + /// + public ImapFolder (ImapFolderConstructorArgs args) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + InitializeProperties (args); + } + + void InitializeProperties (ImapFolderConstructorArgs args) + { + DirectorySeparator = args.DirectorySeparator; + EncodedName = args.EncodedName; + Attributes = args.Attributes; + FullName = args.FullName; + Engine = args.Engine; + Name = args.Name; + } + + /// + /// Get the IMAP command engine. + /// + /// + /// Gets the IMAP command engine. + /// + /// The engine. + internal ImapEngine Engine { + get; private set; + } + + /// + /// Get the encoded name of the folder. + /// + /// + /// Gets the encoded name of the folder. + /// + /// The encoded name. + internal string EncodedName { + get; set; + } + + /// + /// Gets an object that can be used to synchronize access to the IMAP server. + /// + /// + /// Gets an object that can be used to synchronize access to the IMAP server. + /// When using the non-Async methods from multiple threads, it is important to lock the + /// object for thread safety when using the synchronous methods. + /// + /// The lock object. + public override object SyncRoot { + get { return Engine; } + } + + /// + /// Get the threading algorithms supported by the folder. + /// + /// + /// Get the threading algorithms supported by the folder. + /// + /// The supported threading algorithms. + public override HashSet ThreadingAlgorithms { + get { return Engine.ThreadingAlgorithms; } + } + + /// + /// Determine whether or not an supports a feature. + /// + /// + /// Determines whether or not an supports a feature. + /// + /// The desired feature. + /// true if the feature is supported; otherwise, false. + public override bool Supports (FolderFeature feature) + { + switch (feature) { + case FolderFeature.AccessRights: return (Engine.Capabilities & ImapCapabilities.Acl) != 0; + case FolderFeature.Annotations: return AnnotationAccess != AnnotationAccess.None; + case FolderFeature.Metadata: return (Engine.Capabilities & ImapCapabilities.Metadata) != 0; + case FolderFeature.ModSequences: return supportsModSeq; + case FolderFeature.QuickResync: return Engine.QResyncEnabled; + case FolderFeature.Quotas: return (Engine.Capabilities & ImapCapabilities.Quota) != 0; + case FolderFeature.Sorting: return (Engine.Capabilities & ImapCapabilities.Sort) != 0; + case FolderFeature.Threading: return (Engine.Capabilities & ImapCapabilities.Thread) != 0; + case FolderFeature.UTF8: return Engine.UTF8Enabled; + default: return false; + } + } + + void CheckState (bool open, bool rw) + { + if (Engine.IsDisposed) + throw new ObjectDisposedException (nameof (ImapClient)); + + if (!Engine.IsConnected) + throw new ServiceNotConnectedException ("The ImapClient is not connected."); + + if (Engine.State < ImapEngineState.Authenticated) + throw new ServiceNotAuthenticatedException ("The ImapClient is not authenticated."); + + if (open) { + var access = rw ? FolderAccess.ReadWrite : FolderAccess.ReadOnly; + + if (!IsOpen || Access < access) + throw new FolderNotOpenException (FullName, access); + } + } + + void CheckAllowIndexes () + { + // Indexes ("Message Sequence Numbers" or MSNs in the RFCs) and * are not stable while MessageNew/MessageExpunge is registered for SELECTED and therefore should not be used + // https://tools.ietf.org/html/rfc5465#section-5.2 + if (Engine.NotifySelectedNewExpunge) + throw new InvalidOperationException ("Indexes and '*' cannot be used while MessageNew/MessageExpunge is registered with NOTIFY for SELECTED."); + } + + internal void Reset () + { + // basic state + PermanentFlags = MessageFlags.None; + AcceptedFlags = MessageFlags.None; + Access = FolderAccess.None; + + // annotate state + AnnotationAccess = AnnotationAccess.None; + AnnotationScopes = AnnotationScope.None; + MaxAnnotationSize = 0; + + // condstore state + supportsModSeq = false; + HighestModSeq = 0; + } + + /// + /// Notifies the folder that a parent folder has been renamed. + /// + /// + /// Updates the property. + /// + protected override void OnParentFolderRenamed () + { + var oldEncodedName = EncodedName; + + FullName = ParentFolder.FullName + DirectorySeparator + Name; + EncodedName = Engine.EncodeMailboxName (FullName); + Engine.FolderCache.Remove (oldEncodedName); + Engine.FolderCache[EncodedName] = this; + Reset (); + + if (Engine.Selected == this) { + Engine.State = ImapEngineState.Authenticated; + Engine.Selected = null; + OnClosed (); + } + } + + void ProcessResponseCodes (ImapCommand ic, IMailFolder folder, bool throwNotFound = true) + { + bool tryCreate = false; + + foreach (var code in ic.RespCodes) { + switch (code.Type) { + case ImapResponseCodeType.PermanentFlags: + PermanentFlags = ((PermanentFlagsResponseCode) code).Flags; + break; + case ImapResponseCodeType.ReadOnly: + if (code.IsTagged) + Access = FolderAccess.ReadOnly; + break; + case ImapResponseCodeType.ReadWrite: + if (code.IsTagged) + Access = FolderAccess.ReadWrite; + break; + case ImapResponseCodeType.TryCreate: + tryCreate = true; + break; + case ImapResponseCodeType.UidNext: + UidNext = ((UidNextResponseCode) code).Uid; + break; + case ImapResponseCodeType.UidValidity: + var uidValidity = ((UidValidityResponseCode) code).UidValidity; + if (IsOpen) + UpdateUidValidity (uidValidity); + else + UidValidity = uidValidity; + break; + case ImapResponseCodeType.Unseen: + FirstUnread = ((UnseenResponseCode) code).Index; + break; + case ImapResponseCodeType.HighestModSeq: + var highestModSeq = ((HighestModSeqResponseCode) code).HighestModSeq; + supportsModSeq = true; + if (IsOpen) + UpdateHighestModSeq (highestModSeq); + else + HighestModSeq = highestModSeq; + break; + case ImapResponseCodeType.NoModSeq: + supportsModSeq = false; + HighestModSeq = 0; + break; + case ImapResponseCodeType.MailboxId: + // Note: an untagged MAILBOX resp-code is returned on SELECT/EXAMINE while + // a *tagged* MAILBOXID resp-code is returned on CREATE. + if (!code.IsTagged) + Id = ((MailboxIdResponseCode) code).MailboxId; + break; + case ImapResponseCodeType.Annotations: + var annotations = (AnnotationsResponseCode) code; + AnnotationAccess = annotations.Access; + AnnotationScopes = annotations.Scopes; + MaxAnnotationSize = annotations.MaxSize; + break; + } + } + + if (tryCreate && throwNotFound && folder != null) + throw new FolderNotFoundException (folder.FullName); + } + + static ImapResponseCode GetResponseCode (ImapCommand ic, ImapResponseCodeType type) + { + for (int i = 0; i < ic.RespCodes.Count; i++) { + if (ic.RespCodes[i].Type == type) + return ic.RespCodes[i]; + } + + return null; + } + + #region IMailFolder implementation + + /// + /// Gets a value indicating whether the folder is currently open. + /// + /// + /// Gets a value indicating whether the folder is currently open. + /// + /// true if the folder is currently open; otherwise, false. + public override bool IsOpen { + get { return Engine.Selected == this; } + } + + static string SelectOrExamine (FolderAccess access) + { + return access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT"; + } + + static Task QResyncFetchAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + return ic.Folder.OnFetchAsync (engine, index, doAsync, ic.CancellationToken); + } + + async Task OpenAsync (ImapCommand ic, FolderAccess access, bool doAsync, CancellationToken cancellationToken) + { + Reset (); + + if (access == FolderAccess.ReadWrite) { + // Note: if the server does not respond with a PERMANENTFLAGS response, + // then we need to assume all flags are permanent. + PermanentFlags = SettableFlags | MessageFlags.UserDefined; + } else { + PermanentFlags = MessageFlags.None; + } + + try { + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create (access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT", ic); + } catch { + PermanentFlags = MessageFlags.None; + throw; + } + + if (Engine.Selected != null && Engine.Selected != this) { + var folder = Engine.Selected; + + folder.Reset (); + + folder.OnClosed (); + } + + Engine.State = ImapEngineState.Selected; + Engine.Selected = this; + + OnOpened (); + + return Access; + } + + Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, bool doAsync, CancellationToken cancellationToken) + { + if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) + throw new ArgumentOutOfRangeException (nameof (access)); + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.QuickResync) == 0) + throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); + + if (!Supports (FolderFeature.QuickResync)) + throw new InvalidOperationException ("The QRESYNC extension has not been enabled."); + + string qresync; + + if ((Engine.Capabilities & ImapCapabilities.Annotate) != 0 && Engine.QuirksMode != ImapQuirksMode.SunMicrosystems) + qresync = string.Format (CultureInfo.InvariantCulture, "(ANNOTATE QRESYNC ({0} {1}", uidValidity, highestModSeq); + else + qresync = string.Format (CultureInfo.InvariantCulture, "(QRESYNC ({0} {1}", uidValidity, highestModSeq); + + if (uids.Count > 0) { + var set = UniqueIdSet.ToString (uids); + qresync += " " + set; + } + + qresync += "))"; + + var command = string.Format ("{0} %F {1}\r\n", SelectOrExamine (access), qresync); + var ic = new ImapCommand (Engine, cancellationToken, this, command, this); + ic.RegisterUntaggedHandler ("FETCH", QResyncFetchAsync); + + return OpenAsync (ic, access, doAsync, cancellationToken); + } + + /// + /// Open the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The QRESYNC feature has not been enabled. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override FolderAccess Open (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)) + { + return OpenAsync (access, uidValidity, highestModSeq, uids, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously open the folder using the requested folder access. + /// + /// + /// This variant of the + /// method is meant for quick resynchronization of the folder. Before calling this method, + /// the method MUST be called. + /// You should also make sure to add listeners to the and + /// events to get notifications of changes since + /// the last time the folder was opened. + /// + /// The state of the folder. + /// The requested folder access. + /// The last known value. + /// The last known value. + /// The last known list of unique message identifiers. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The QRESYNC feature has not been enabled. + /// + /// + /// The IMAP server does not support the QRESYNC extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task OpenAsync (FolderAccess access, uint uidValidity, ulong highestModSeq, IList uids, CancellationToken cancellationToken = default (CancellationToken)) + { + return OpenAsync (access, uidValidity, highestModSeq, uids, true, cancellationToken); + } + + Task OpenAsync (FolderAccess access, bool doAsync, CancellationToken cancellationToken) + { + if (access != FolderAccess.ReadOnly && access != FolderAccess.ReadWrite) + throw new ArgumentOutOfRangeException (nameof (access)); + + CheckState (false, false); + + var @params = string.Empty; + + if ((Engine.Capabilities & ImapCapabilities.CondStore) != 0) + @params += "CONDSTORE"; + if ((Engine.Capabilities & ImapCapabilities.Annotate) != 0 && Engine.QuirksMode != ImapQuirksMode.SunMicrosystems) + @params += " ANNOTATE"; + + if (@params.Length > 0) + @params = " (" + @params.TrimStart () + ")"; + + var command = string.Format ("{0} %F{1}\r\n", SelectOrExamine (access), @params); + var ic = new ImapCommand (Engine, cancellationToken, this, command, this); + + return OpenAsync (ic, access, doAsync, cancellationToken); + } + + /// + /// Open the folder using the requested folder access. + /// + /// + /// Opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override FolderAccess Open (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)) + { + return OpenAsync (access, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously open the folder using the requested folder access. + /// + /// + /// Opens the folder using the requested folder access. + /// + /// The state of the folder. + /// The requested folder access. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task OpenAsync (FolderAccess access, CancellationToken cancellationToken = default (CancellationToken)) + { + return OpenAsync (access, true, cancellationToken); + } + + async Task CloseAsync (bool expunge, bool doAsync, CancellationToken cancellationToken) + { + CheckState (true, expunge); + + ImapCommand ic; + + if (expunge) { + ic = Engine.QueueCommand (cancellationToken, this, "CLOSE\r\n"); + } else if ((Engine.Capabilities & ImapCapabilities.Unselect) != 0) { + ic = Engine.QueueCommand (cancellationToken, this, "UNSELECT\r\n"); + } else { + ic = null; + } + + if (ic != null) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create (expunge ? "CLOSE" : "UNSELECT", ic); + } + + Reset (); + + if (Engine.Selected == this) { + Engine.State = ImapEngineState.Authenticated; + Engine.Selected = null; + OnClosed (); + } + } + + /// + /// Close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Closes the folder, optionally expunging the messages marked for deletion. + /// + /// If set to true, expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Close (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)) + { + CloseAsync (expunge, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously close the folder, optionally expunging the messages marked for deletion. + /// + /// + /// Closes the folder, optionally expunging the messages marked for deletion. + /// + /// An asynchronous task context. + /// If set to true, expunge. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CloseAsync (bool expunge = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return CloseAsync (expunge, true, cancellationToken); + } + + async Task GetCreatedFolderAsync (string encodedName, string id, bool specialUse, bool doAsync, CancellationToken cancellationToken) + { + var ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", encodedName); + var list = new List (); + ImapFolder folder; + + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("LIST", ic); + + if ((folder = ImapEngine.GetFolder (list, encodedName)) != null) { + folder.ParentFolder = this; + folder.Id = id; + + if (specialUse) + Engine.AssignSpecialFolder (folder); + } + + return folder; + } + + async Task CreateAsync (string name, bool isMessageFolder, bool doAsync, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); + + CheckState (false, false); + + if (!string.IsNullOrEmpty (FullName) && DirectorySeparator == '\0') + throw new InvalidOperationException ("Cannot create child folders."); + + var fullName = !string.IsNullOrEmpty (FullName) ? FullName + DirectorySeparator + name : name; + var encodedName = Engine.EncodeMailboxName (fullName); + var createName = encodedName; + + if (!isMessageFolder && Engine.QuirksMode != ImapQuirksMode.GMail) + createName += DirectorySeparator; + + var ic = Engine.QueueCommand (cancellationToken, null, "CREATE %S\r\n", createName); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok && GetResponseCode (ic, ImapResponseCodeType.AlreadyExists) == null) + throw ImapCommandException.Create ("CREATE", ic); + + var code = (MailboxIdResponseCode) GetResponseCode (ic, ImapResponseCodeType.MailboxId); + var id = code?.MailboxId; + + var created = await GetCreatedFolderAsync (encodedName, id, false, doAsync, cancellationToken).ConfigureAwait (false); + + Engine.OnFolderCreated (created); + + return created; + } + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IMailFolder Create (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)) + { + return CreateAsync (name, isMessageFolder, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// true if the folder will be used to contain messages; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CreateAsync (string name, bool isMessageFolder, CancellationToken cancellationToken = default (CancellationToken)) + { + return CreateAsync (name, isMessageFolder, true, cancellationToken); + } + + async Task CreateAsync (string name, IEnumerable specialUses, bool doAsync, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); + + if (specialUses == null) + throw new ArgumentNullException (nameof (specialUses)); + + CheckState (false, false); + + if (!string.IsNullOrEmpty (FullName) && DirectorySeparator == '\0') + throw new InvalidOperationException ("Cannot create child folders."); + + if ((Engine.Capabilities & ImapCapabilities.CreateSpecialUse) == 0) + throw new NotSupportedException ("The IMAP server does not support the CREATE-SPECIAL-USE extension."); + + var uses = new StringBuilder (); + uint used = 0; + + foreach (var use in specialUses) { + var bit = (uint) (1 << ((int) use)); + + if ((used & bit) != 0) + continue; + + used |= bit; + + if (uses.Length > 0) + uses.Append (' '); + + switch (use) { + case SpecialFolder.All: uses.Append ("\\All"); break; + case SpecialFolder.Archive: uses.Append ("\\Archive"); break; + case SpecialFolder.Drafts: uses.Append ("\\Drafts"); break; + case SpecialFolder.Flagged: uses.Append ("\\Flagged"); break; + case SpecialFolder.Important: uses.Append ("\\Important"); break; + case SpecialFolder.Junk: uses.Append ("\\Junk"); break; + case SpecialFolder.Sent: uses.Append ("\\Sent"); break; + case SpecialFolder.Trash: uses.Append ("\\Trash"); break; + default: if (uses.Length > 0) uses.Length--; break; + } + } + + var fullName = !string.IsNullOrEmpty (FullName) ? FullName + DirectorySeparator + name : name; + var encodedName = Engine.EncodeMailboxName (fullName); + string command; + + if (uses.Length > 0) + command = string.Format ("CREATE %S (USE ({0}))\r\n", uses); + else + command = "CREATE %S\r\n"; + + var ic = Engine.QueueCommand (cancellationToken, null, command, encodedName); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("CREATE", ic); + + var code = (MailboxIdResponseCode) GetResponseCode (ic, ImapResponseCodeType.MailboxId); + var id = code?.MailboxId; + + var created = await GetCreatedFolderAsync (encodedName, id, true, doAsync, cancellationToken).ConfigureAwait (false); + + Engine.OnFolderCreated (created); + + return created; + } + + /// + /// Create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The IMAP server does not support the CREATE-SPECIAL-USE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IMailFolder Create (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)) + { + return CreateAsync (name, specialUses, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously create a new subfolder with the given name. + /// + /// + /// Creates a new subfolder with the given name. + /// + /// The created folder. + /// The name of the folder to create. + /// A list of special uses for the folder being created. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is nil, and thus child folders cannot be created. + /// + /// + /// The IMAP server does not support the CREATE-SPECIAL-USE extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CreateAsync (string name, IEnumerable specialUses, CancellationToken cancellationToken = default (CancellationToken)) + { + return CreateAsync (name, specialUses, true, cancellationToken); + } + + async Task RenameAsync (IMailFolder parent, string name, bool doAsync, CancellationToken cancellationToken) + { + if (parent == null) + throw new ArgumentNullException (nameof (parent)); + + if (!(parent is ImapFolder) || ((ImapFolder) parent).Engine != Engine) + throw new ArgumentException ("The parent folder does not belong to this ImapClient.", nameof (parent)); + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name is not a legal folder name.", nameof (name)); + + if (IsNamespace || (Attributes & FolderAttributes.Inbox) != 0) + throw new InvalidOperationException ("Cannot rename this folder."); + + CheckState (false, false); + + string newFullName; + + if (!string.IsNullOrEmpty (parent.FullName)) + newFullName = parent.FullName + parent.DirectorySeparator + name; + else + newFullName = name; + + var encodedName = Engine.EncodeMailboxName (newFullName); + var ic = Engine.QueueCommand (cancellationToken, null, "RENAME %F %S\r\n", this, encodedName); + var oldFullName = FullName; + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("RENAME", ic); + + Engine.FolderCache.Remove (EncodedName); + Engine.FolderCache[encodedName] = this; + + ParentFolder = parent; + + FullName = Engine.DecodeMailboxName (encodedName); + EncodedName = encodedName; + Name = name; + + Reset (); + + if (Engine.Selected == this) { + Engine.State = ImapEngineState.Authenticated; + Engine.Selected = null; + OnClosed (); + } + + OnRenamed (oldFullName, FullName); + } + + /// + /// Rename the folder to exist with a new name under a new parent folder. + /// + /// + /// Renames the folder to exist with a new name under a new parent folder. + /// + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Rename (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)) + { + RenameAsync (parent, name, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously rename the folder to exist with a new name under a new parent folder. + /// + /// + /// Renames the folder to exist with a new name under a new parent folder. + /// + /// An awaitable task. + /// The new parent folder. + /// The new name of the folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not belong to the . + /// -or- + /// is not a legal folder name. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The folder cannot be renamed (it is either a namespace or the Inbox). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task RenameAsync (IMailFolder parent, string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return RenameAsync (parent, name, true, cancellationToken); + } + + async Task DeleteAsync (bool doAsync, CancellationToken cancellationToken) + { + if (IsNamespace || (Attributes & FolderAttributes.Inbox) != 0) + throw new InvalidOperationException ("Cannot delete this folder."); + + CheckState (false, false); + + var ic = Engine.QueueCommand (cancellationToken, null, "DELETE %F\r\n", this); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("DELETE", ic); + + Reset (); + + if (Engine.Selected == this) { + Engine.State = ImapEngineState.Authenticated; + Engine.Selected = null; + OnClosed (); + } + + Attributes |= FolderAttributes.NonExistent; + OnDeleted (); + } + + /// + /// Delete the folder on the IMAP server. + /// + /// + /// Deletes the folder on the IMAP server. + /// This method will not delete any child folders. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Delete (CancellationToken cancellationToken = default (CancellationToken)) + { + DeleteAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously delete the folder on the IMAP server. + /// + /// + /// Deletes the folder on the IMAP server. + /// This method will not delete any child folders. + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The folder cannot be deleted (it is either a namespace or the Inbox). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task DeleteAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return DeleteAsync (true, cancellationToken); + } + + async Task SubscribeAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + var ic = Engine.QueueCommand (cancellationToken, null, "SUBSCRIBE %F\r\n", this); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SUBSCRIBE", ic); + + if ((Attributes & FolderAttributes.Subscribed) == 0) { + Attributes |= FolderAttributes.Subscribed; + + OnSubscribed (); + } + } + + /// + /// Subscribe the folder. + /// + /// + /// Subscribes the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Subscribe (CancellationToken cancellationToken = default (CancellationToken)) + { + SubscribeAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously subscribe the folder. + /// + /// + /// Subscribes the folder. + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return SubscribeAsync (true, cancellationToken); + } + + async Task UnsubscribeAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + var ic = Engine.QueueCommand (cancellationToken, null, "UNSUBSCRIBE %F\r\n", this); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("UNSUBSCRIBE", ic); + + if ((Attributes & FolderAttributes.Subscribed) != 0) { + Attributes &= ~FolderAttributes.Subscribed; + + OnUnsubscribed (); + } + } + + /// + /// Unsubscribe the folder. + /// + /// + /// Unsubscribes the folder. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Unsubscribe (CancellationToken cancellationToken = default (CancellationToken)) + { + UnsubscribeAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously unsubscribe the folder. + /// + /// + /// Unsubscribes the folder. + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task UnsubscribeAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return UnsubscribeAsync (true, cancellationToken); + } + + async Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var pattern = new StringBuilder (EncodedName.Length + 2); + pattern.Append (EncodedName); + for (int i = 0; i < EncodedName.Length; i++) { + if (pattern[i] == '*') + pattern[i] = '%'; + } + if (pattern.Length > 0) + pattern.Append (DirectorySeparator); + pattern.Append ('%'); + + var children = new List (); + var status = items != StatusItems.None; + var list = new List (); + var command = new StringBuilder (); + var returnsSubscribed = false; + var lsub = subscribedOnly; + + if (subscribedOnly) { + if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append ("LIST (SUBSCRIBED)"); + returnsSubscribed = true; + lsub = false; + } else { + command.Append ("LSUB"); + } + } else { + command.Append ("LIST"); + } + + command.Append (" \"\" %S"); + + if (!lsub) { + if (items != StatusItems.None && (Engine.Capabilities & ImapCapabilities.ListStatus) != 0) { + command.Append (" RETURN ("); + + if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN "); + } + + command.AppendFormat ("STATUS ({0})", Engine.GetStatusQuery (items)); + command.Append (')'); + status = false; + } else if ((Engine.Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN ("); + if (!subscribedOnly) { + command.Append ("SUBSCRIBED "); + returnsSubscribed = true; + } + command.Append ("CHILDREN"); + command.Append (')'); + } + } + + command.Append ("\r\n"); + + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), pattern.ToString ()); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; + ic.Lsub = lsub; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + // Note: Due to the fact that folders can contain wildcards in them, we'll need to + // filter out any folders that are not children of this folder. + var prefix = FullName.Length > 0 ? FullName + DirectorySeparator : string.Empty; + prefix = ImapUtils.CanonicalizeMailboxName (prefix, DirectorySeparator); + var unparented = false; + + foreach (var folder in list) { + var canonicalFullName = ImapUtils.CanonicalizeMailboxName (folder.FullName, folder.DirectorySeparator); + var canonicalName = ImapUtils.IsInbox (folder.FullName) ? "INBOX" : folder.Name; + + if (!canonicalFullName.StartsWith (prefix, StringComparison.Ordinal)) { + unparented |= folder.ParentFolder == null; + continue; + } + + if (string.Compare (canonicalFullName, prefix.Length, canonicalName, 0, canonicalName.Length, StringComparison.Ordinal) != 0) { + unparented |= folder.ParentFolder == null; + continue; + } + + folder.ParentFolder = this; + children.Add (folder); + } + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic); + + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + if (unparented) + await Engine.LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + + if (status) { + for (int i = 0; i < children.Count; i++) { + if (children[i].Exists) + await ((ImapFolder) children[i]).StatusAsync (items, doAsync, false, cancellationToken).ConfigureAwait (false); + } + } + + return children; + } + + /// + /// Get the subfolders. + /// + /// + /// Gets the subfolders. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList GetSubfolders (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfoldersAsync (items, subscribedOnly, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the subfolders. + /// + /// + /// Gets the subfolders. + /// + /// The subfolders. + /// The status items to pre-populate. + /// If set to true, only subscribed folders will be listed. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> GetSubfoldersAsync (StatusItems items, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfoldersAsync (items, subscribedOnly, true, cancellationToken); + } + + async Task GetSubfolderAsync (string name, bool doAsync, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!Engine.IsValidMailboxName (name, DirectorySeparator)) + throw new ArgumentException ("The name of the subfolder is invalid.", nameof (name)); + + CheckState (false, false); + + var fullName = FullName.Length > 0 ? FullName + DirectorySeparator + name : name; + var encodedName = Engine.EncodeMailboxName (fullName); + List list; + ImapFolder folder; + + if (Engine.GetCachedFolder (encodedName, out folder)) + return folder; + + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var pattern = encodedName.Replace ('*', '%'); + + var ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", pattern); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list = new List (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("LIST", ic); + + if ((folder = ImapEngine.GetFolder (list, encodedName)) != null) + folder.ParentFolder = this; + + if (list.Count > 1 || folder == null) { + // Note: if any folders returned in the LIST command are unparented, have the ImapEngine look up their + // parent folders now so that they are not left in an inconsistent state. + await Engine.LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + } + + if (folder == null) + throw new FolderNotFoundException (fullName); + + return folder; + } + + /// + /// Get the specified subfolder. + /// + /// + /// Gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IMailFolder GetSubfolder (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfolderAsync (name, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified subfolder. + /// + /// + /// Gets the specified subfolder. + /// + /// The subfolder. + /// The name of the subfolder. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is either an empty string or contains the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The requested folder could not be found. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetSubfolderAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetSubfolderAsync (name, true, cancellationToken); + } + + async Task CheckAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckState (true, false); + + var ic = Engine.QueueCommand (cancellationToken, this, "CHECK\r\n"); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("CHECK", ic); + } + + /// + /// Force the server to sync its in-memory state with its disk state. + /// + /// + /// The CHECK command forces the IMAP server to sync its + /// in-memory state with its disk state. + /// For more information about the CHECK command, see + /// rfc350101. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Check (CancellationToken cancellationToken = default (CancellationToken)) + { + CheckAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously force the server to sync its in-memory state with its disk state. + /// + /// + /// The CHECK command forces the IMAP server to sync its + /// in-memory state with its disk state. + /// For more information about the CHECK command, see + /// rfc350101. + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CheckAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return CheckAsync (true, cancellationToken); + } + + internal async Task StatusAsync (StatusItems items, bool doAsync, bool throwNotFound, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Status) == 0) + throw new NotSupportedException ("The IMAP server does not support the STATUS command."); + + CheckState (false, false); + + if (items == StatusItems.None) + return; + + var command = string.Format ("STATUS %F ({0})\r\n", Engine.GetStatusQuery (items)); + var ic = Engine.QueueCommand (cancellationToken, null, command, this); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this, throwNotFound); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STATUS", ic); + } + + /// + /// Update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// For more information about the STATUS command, see + /// rfc3501. + /// + /// The items to update. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The IMAP server does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Status (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + StatusAsync (items, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously update the values of the specified items. + /// + /// + /// Updates the values of the specified items. + /// The method + /// MUST NOT be used on a folder that is already in the opened state. Instead, other ways + /// of getting the desired information should be used. + /// For example, a common use for the + /// method is to get the number of unread messages in the folder. When the folder is open, however, it is + /// possible to use the + /// method to query for the list of unread messages. + /// For more information about the STATUS command, see + /// rfc3501. + /// + /// An awaitable task. + /// The items to update. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The does not exist. + /// + /// + /// The IMAP server does not support the STATUS command. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task StatusAsync (StatusItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return StatusAsync (items, true, true, cancellationToken); + } + + static async Task ReadStringTokenAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.Literal: return await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + case ImapTokenType.QString: return (string) token.Value; + case ImapTokenType.Atom: return (string) token.Value; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + static async Task UntaggedAclAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); + var acl = (AccessControlList) ic.UserData; + string name, rights; + ImapToken token; + + // read the mailbox name + await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + do { + name = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + rights = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + acl.Add (new AccessControl (name, rights)); + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + async Task GetAccessControlListAsync (bool doAsync, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETACL %F\r\n", this); + ic.RegisterUntaggedHandler ("ACL", UntaggedAclAsync); + ic.UserData = new AccessControlList (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETACL", ic); + + return (AccessControlList) ic.UserData; + } + + /// + /// Get the complete access control list for the folder. + /// + /// + /// Gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override AccessControlList GetAccessControlList (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetAccessControlListAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the complete access control list for the folder. + /// + /// + /// Gets the complete access control list for the folder. + /// + /// The access control list. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task GetAccessControlListAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetAccessControlListAsync (true, cancellationToken); + } + + static async Task UntaggedListRightsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData; + ImapToken token; + + // read the mailbox name + await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + // read the identity name + await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + do { + var rights = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + access.AddRange (rights); + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + async Task GetAccessRightsAsync (string name, bool doAsync, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "LISTRIGHTS %F %S\r\n", this, name); + ic.RegisterUntaggedHandler ("LISTRIGHTS", UntaggedListRightsAsync); + ic.UserData = new AccessRights (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("LISTRIGHTS", ic); + + return (AccessRights) ic.UserData; + } + + /// + /// Get the access rights for a particular identifier. + /// + /// + /// Gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override AccessRights GetAccessRights (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetAccessRightsAsync (name, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the access rights for a particular identifier. + /// + /// + /// Gets the access rights for a particular identifier. + /// + /// The access rights. + /// The identifier name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task GetAccessRightsAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetAccessRightsAsync (name, true, cancellationToken); + } + + static async Task UntaggedMyRightsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData; + + // read the mailbox name + await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + // read the access rights + access.AddRange (await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false)); + } + + async Task GetMyAccessRightsAsync (bool doAsync, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = new ImapCommand (Engine, cancellationToken, null, "MYRIGHTS %F\r\n", this); + ic.RegisterUntaggedHandler ("MYRIGHTS", UntaggedMyRightsAsync); + ic.UserData = new AccessRights (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("MYRIGHTS", ic); + + return (AccessRights) ic.UserData; + } + + /// + /// Get the access rights for the current authenticated user. + /// + /// + /// Gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override AccessRights GetMyAccessRights (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMyAccessRightsAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the access rights for the current authenticated user. + /// + /// + /// Gets the access rights for the current authenticated user. + /// + /// The access rights. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task GetMyAccessRightsAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMyAccessRightsAsync (true, cancellationToken); + } + + async Task ModifyAccessRightsAsync (string name, AccessRights rights, string action, bool doAsync, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = Engine.QueueCommand (cancellationToken, null, "SETACL %F %S %S\r\n", this, name, action + rights); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SETACL", ic); + } + + /// + /// Add access rights for the specified identity. + /// + /// + /// Adds the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// No rights were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override void AddAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + if (rights.Count == 0) + throw new ArgumentException ("No rights were specified.", nameof (rights)); + + ModifyAccessRightsAsync (name, rights, "+", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously add access rights for the specified identity. + /// + /// + /// Adds the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// No rights were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task AddAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + if (rights.Count == 0) + throw new ArgumentException ("No rights were specified.", nameof (rights)); + + return ModifyAccessRightsAsync (name, rights, "+", true, cancellationToken); + } + + /// + /// Remove access rights for the specified identity. + /// + /// + /// Removes the given access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// No rights were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override void RemoveAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + if (rights.Count == 0) + throw new ArgumentException ("No rights were specified.", nameof (rights)); + + ModifyAccessRightsAsync (name, rights, "-", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove access rights for the specified identity. + /// + /// + /// Removes the given access rights for the specified identity. + /// + /// An asynchronous task context. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// No rights were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task RemoveAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + if (rights.Count == 0) + throw new ArgumentException ("No rights were specified.", nameof (rights)); + + return ModifyAccessRightsAsync (name, rights, "-", true, cancellationToken); + } + + /// + /// Set the access rights for the specified identity. + /// + /// + /// Sets the access rights for the specified identity. + /// + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override void SetAccessRights (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + ModifyAccessRightsAsync (name, rights, string.Empty, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the access rights for the specified identity. + /// + /// + /// Sets the access rights for the specified identity. + /// + /// An awaitable task. + /// The identity name. + /// The access rights. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task SetAccessRightsAsync (string name, AccessRights rights, CancellationToken cancellationToken = default (CancellationToken)) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (rights == null) + throw new ArgumentNullException (nameof (rights)); + + return ModifyAccessRightsAsync (name, rights, string.Empty, true, cancellationToken); + } + + async Task RemoveAccessAsync (string name, bool doAsync, CancellationToken cancellationToken) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) + throw new NotSupportedException ("The IMAP server does not support the ACL extension."); + + CheckState (false, false); + + var ic = Engine.QueueCommand (cancellationToken, null, "DELETEACL %F %S\r\n", this, name); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("DELETEACL", ic); + } + + /// + /// Remove all access rights for the given identity. + /// + /// + /// Removes all access rights for the given identity. + /// + /// The identity name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override void RemoveAccess (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + RemoveAccessAsync (name, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove all access rights for the given identity. + /// + /// + /// Removes all access rights for the given identity. + /// + /// An awaitable task. + /// The identity name. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the ACL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The command failed. + /// + public override Task RemoveAccessAsync (string name, CancellationToken cancellationToken = default (CancellationToken)) + { + return RemoveAccessAsync (name, true, cancellationToken); + } + + async Task GetMetadataAsync (MetadataTag tag, bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETMETADATA %F %S\r\n", this, tag.Id); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + var metadata = new MetadataCollection (); + ic.UserData = metadata; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETMETADATA", ic); + + string value = null; + + for (int i = 0; i < metadata.Count; i++) { + if (metadata[i].EncodedName == EncodedName && metadata[i].Tag.Id == tag.Id) { + value = metadata[i].Value; + metadata.RemoveAt (i); + break; + } + } + + Engine.ProcessMetadataChanges (metadata); + + return value; + } + + /// + /// Get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (tag, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata value. + /// The metadata tag. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMetadataAsync (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (tag, true, cancellationToken); + } + + async Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (tags == null) + throw new ArgumentNullException (nameof (tags)); + + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + var command = new StringBuilder ("GETMETADATA %F"); + var args = new List (); + bool hasOptions = false; + + if (options.MaxSize.HasValue || options.Depth != 0) { + command.Append (" ("); + if (options.MaxSize.HasValue) + command.AppendFormat ("MAXSIZE {0} ", options.MaxSize.Value); + if (options.Depth > 0) + command.AppendFormat ("DEPTH {0} ", options.Depth == int.MaxValue ? "infinity" : "1"); + command[command.Length - 1] = ')'; + command.Append (' '); + hasOptions = true; + } + + args.Add (this); + + int startIndex = command.Length; + foreach (var tag in tags) { + command.Append (" %S"); + args.Add (tag.Id); + } + + if (hasOptions) { + command[startIndex] = '('; + command.Append (')'); + } + + command.Append ("\r\n"); + + if (args.Count == 1) + return new MetadataCollection (); + + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + ic.UserData = new MetadataCollection (); + options.LongEntries = 0; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETMETADATA", ic); + + var metadata = (MetadataResponseCode) GetResponseCode (ic, ImapResponseCodeType.Metadata); + if (metadata != null && metadata.SubType == MetadataResponseCodeSubType.LongEntries) + options.LongEntries = metadata.Value; + + return Engine.FilterMetadata ((MetadataCollection) ic.UserData, EncodedName); + } + + /// + /// Get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (options, tags, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified metadata. + /// + /// + /// Gets the specified metadata. + /// + /// The requested metadata. + /// The metadata options. + /// The metadata tags. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMetadataAsync (options, tags, true, cancellationToken); + } + + async Task SetMetadataAsync (MetadataCollection metadata, bool doAsync, CancellationToken cancellationToken) + { + if (metadata == null) + throw new ArgumentNullException (nameof (metadata)); + + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Metadata) == 0) + throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); + + if (metadata.Count == 0) + return; + + var command = new StringBuilder ("SETMETADATA %F ("); + var args = new List (); + + args.Add (this); + + for (int i = 0; i < metadata.Count; i++) { + if (i > 0) + command.Append (' '); + + if (metadata[i].Value != null) { + command.Append ("%S %S"); + args.Add (metadata[i].Tag.Id); + args.Add (metadata[i].Value); + } else { + command.Append ("%S NIL"); + args.Add (metadata[i].Tag.Id); + } + } + command.Append (")\r\n"); + + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SETMETADATA", ic); + } + + /// + /// Set the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) + { + SetMetadataAsync (metadata, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously set the specified metadata. + /// + /// + /// Sets the specified metadata. + /// + /// An asynchronous task context. + /// The metadata. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the METADATA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetMetadataAsync (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetMetadataAsync (metadata, true, cancellationToken); + } + + class Quota + { + public uint? MessageLimit; + public uint? StorageLimit; + public uint? CurrentMessageCount; + public uint? CurrentStorageSize; + } + + class QuotaContext + { + public QuotaContext () + { + Quotas = new Dictionary (); + QuotaRoots = new List (); + } + + public IList QuotaRoots { + get; private set; + } + + public IDictionary Quotas { + get; private set; + } + } + + static async Task UntaggedQuotaRootAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); + var ctx = (QuotaContext) ic.UserData; + + // The first token should be the mailbox name + await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + + // ...followed by 0 or more quota roots + var token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + while (token.Type != ImapTokenType.Eoln) { + var root = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + ctx.QuotaRoots.Add (root); + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + } + + static async Task UntaggedQuotaAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); + var quotaRoot = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + var ctx = (QuotaContext) ic.UserData; + var quota = new Quota (); + + var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + uint used, limit; + string resource; + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + resource = (string) token.Value; + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + used = ImapEngine.ParseNumber (token, false, format, token); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + limit = ImapEngine.ParseNumber (token, false, format, token); + + switch (resource.ToUpperInvariant ()) { + case "MESSAGE": + quota.CurrentMessageCount = used; + quota.MessageLimit = limit; + break; + case "STORAGE": + quota.CurrentStorageSize = used; + quota.StorageLimit = limit; + break; + } + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + // read the closing paren + await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ctx.Quotas[quotaRoot] = quota; + } + + async Task GetQuotaAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) + throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); + + var ic = new ImapCommand (Engine, cancellationToken, null, "GETQUOTAROOT %F\r\n", this); + var ctx = new QuotaContext (); + + ic.RegisterUntaggedHandler ("QUOTAROOT", UntaggedQuotaRootAsync); + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("GETQUOTAROOT", ic); + + for (int i = 0; i < ctx.QuotaRoots.Count; i++) { + var encodedName = ctx.QuotaRoots[i]; + ImapFolder quotaRoot; + Quota quota; + + if (!ctx.Quotas.TryGetValue (encodedName, out quota)) + continue; + + quotaRoot = await Engine.GetQuotaRootFolderAsync (encodedName, doAsync, cancellationToken).ConfigureAwait (false); + + return new FolderQuota (quotaRoot) { + CurrentMessageCount = quota.CurrentMessageCount, + CurrentStorageSize = quota.CurrentStorageSize, + MessageLimit = quota.MessageLimit, + StorageLimit = quota.StorageLimit + }; + } + + return new FolderQuota (null); + } + + /// + /// Get the quota information for the folder. + /// + /// + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the QUOTA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override FolderQuota GetQuota (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetQuotaAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the quota information for the folder. + /// + /// + /// Gets the quota information for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the QUOTA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetQuotaAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetQuotaAsync (true, cancellationToken); + } + + async Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, bool doAsync, CancellationToken cancellationToken) + { + CheckState (false, false); + + if ((Engine.Capabilities & ImapCapabilities.Quota) == 0) + throw new NotSupportedException ("The IMAP server does not support the QUOTA extension."); + + var command = new StringBuilder ("SETQUOTA %F ("); + if (messageLimit.HasValue) + command.AppendFormat ("MESSAGE {0} ", messageLimit.Value); + if (storageLimit.HasValue) + command.AppendFormat ("STORAGE {0} ", storageLimit.Value); + command[command.Length - 1] = ')'; + command.Append ("\r\n"); + + var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), this); + var ctx = new QuotaContext (); + Quota quota; + + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SETQUOTA", ic); + + if (ctx.Quotas.TryGetValue (EncodedName, out quota)) { + return new FolderQuota (this) { + CurrentMessageCount = quota.CurrentMessageCount, + CurrentStorageSize = quota.CurrentStorageSize, + MessageLimit = quota.MessageLimit, + StorageLimit = quota.StorageLimit + }; + } + + return new FolderQuota (null); + } + + /// + /// Set the quota limits for the folder. + /// + /// + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the QUOTA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override FolderQuota SetQuota (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetQuotaAsync (messageLimit, storageLimit, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously set the quota limits for the folder. + /// + /// + /// Sets the quota limits for the folder. + /// To determine if a quotas are supported, check the + /// property. + /// + /// The folder quota. + /// If not null, sets the maximum number of messages to allow. + /// If not null, sets the maximum storage size (in kilobytes). + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The IMAP server does not support the QUOTA extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetQuotaAsync (messageLimit, storageLimit, true, cancellationToken); + } + + async Task ExpungeAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckState (true, true); + + var ic = Engine.QueueCommand (cancellationToken, this, "EXPUNGE\r\n"); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("EXPUNGE", ic); + } + + /// + /// Expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// The EXPUNGE command permanently removes all messages in the folder + /// that have the flag set. + /// For more information about the EXPUNGE command, see + /// rfc3501. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Expunge (CancellationToken cancellationToken = default (CancellationToken)) + { + ExpungeAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously expunge the folder, permanently removing all messages marked for deletion. + /// + /// + /// The EXPUNGE command permanently removes all messages in the folder + /// that have the flag set. + /// For more information about the EXPUNGE command, see + /// rfc3501. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ExpungeAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return ExpungeAsync (true, cancellationToken); + } + + async Task ExpungeAsync (IList uids, bool doAsync, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + CheckState (true, true); + + if (uids.Count == 0) + return; + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + // get the list of messages marked for deletion that should not be expunged + var query = SearchQuery.Deleted.And (SearchQuery.Not (SearchQuery.Uids (uids))); + var unmark = await SearchAsync (query, doAsync, false, cancellationToken).ConfigureAwait (false); + + if (unmark.Count > 0) { + // clear the \Deleted flag on all messages except the ones that are to be expunged + await ModifyFlagsAsync (unmark, null, MessageFlags.Deleted, null, "-FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + } + + // expunge the folder + await ExpungeAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (unmark.Count > 0) { + // restore the \Deleted flags + await ModifyFlagsAsync (unmark, null, MessageFlags.Deleted, null, "+FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + } + + return; + } + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID EXPUNGE %s\r\n", uids)) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("EXPUNGE", ic); + } + } + + /// + /// Expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Expunges the specified uids, permanently removing them from the folder. + /// If the IMAP server supports the UIDPLUS extension (check the + /// for the + /// flag), then this operation is atomic. Otherwise, MailKit implements this operation + /// by first searching for the full list of message uids in the folder that are marked for + /// deletion, unmarking the set of message uids that are not within the specified list of + /// uids to be be expunged, expunging the folder (thus expunging the requested uids), and + /// finally restoring the deleted flag on the collection of message uids that were originally + /// marked for deletion that were not included in the list of uids provided. For this reason, + /// it is advisable for clients that wish to maintain state to implement this themselves when + /// the IMAP server does not support the UIDPLUS extension. + /// For more information about the UID EXPUNGE command, see + /// rfc4315. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. + /// + /// The message uids. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Expunge (IList uids, CancellationToken cancellationToken = default (CancellationToken)) + { + ExpungeAsync (uids, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously expunge the specified uids, permanently removing them from the folder. + /// + /// + /// Expunges the specified uids, permanently removing them from the folder. + /// If the IMAP server supports the UIDPLUS extension (check the + /// for the + /// flag), then this operation is atomic. Otherwise, MailKit implements this operation + /// by first searching for the full list of message uids in the folder that are marked for + /// deletion, unmarking the set of message uids that are not within the specified list of + /// uids to be be expunged, expunging the folder (thus expunging the requested uids), and + /// finally restoring the deleted flag on the collection of message uids that were originally + /// marked for deletion that were not included in the list of uids provided. For this reason, + /// it is advisable for clients that wish to maintain state to implement this themselves when + /// the IMAP server does not support the UIDPLUS extension. + /// For more information about the UID EXPUNGE command, see + /// rfc4315. + /// Normally, a event will be emitted + /// for each message that is expunged. However, if the IMAP server supports the QRESYNC extension + /// and it has been enabled via the + /// method, then the event will be emitted rather than + /// the event. + /// + /// An asynchronous task context. + /// The message uids. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ExpungeAsync (IList uids, CancellationToken cancellationToken = default (CancellationToken)) + { + return ExpungeAsync (uids, true, cancellationToken); + } + + ImapCommand QueueAppend (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken, ITransferProgress progress) + { + var builder = new StringBuilder ("APPEND %F "); + var list = new List (); + + list.Add (this); + + if ((flags & SettableFlags) != 0) + builder.AppendFormat ("{0} ", ImapUtils.FormatFlagsList (flags, 0)); + + if (date.HasValue) + builder.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (date.Value)); + + if (annotations != null && annotations.Count > 0) { + ImapUtils.FormatAnnotations (builder, annotations, list, false); + + if (builder[builder.Length - 1] != ' ') + builder.Append (' '); + } + + builder.Append ("%L\r\n"); + list.Add (message); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, options, command, args); + ic.Progress = progress; + + Engine.QueueCommand (ic); + + return ic; + } + + async Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + CheckState (false, false); + + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; + + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); + + var ic = QueueAppend (format, message, flags, date, annotations, cancellationToken, progress); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("APPEND", ic); + + var append = (AppendUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.AppendUid); + + if (append != null) + return append.UidSet[0]; + + return null; + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, null, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, null, null, true, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, date, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, date, null, true, cancellationToken, progress); + } + + /// + /// Append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Append (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, date, annotations, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously append the specified message to the folder. + /// + /// + /// Appends the specified message to the folder and returns the UniqueId assigned to the message. + /// + /// The UID of the appended message, if available; otherwise, null. + /// The formatting options. + /// The message. + /// The message flags. + /// The received date of the message. + /// The message annotations. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AppendAsync (FormatOptions options, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, message, flags, date, annotations, true, cancellationToken, progress); + } + + ImapCommand QueueMultiAppend (FormatOptions options, IList messages, IList flags, IList dates, IList> annotations, CancellationToken cancellationToken, ITransferProgress progress) + { + var builder = new StringBuilder ("APPEND %F"); + var list = new List (); + + list.Add (this); + + for (int i = 0; i < messages.Count; i++) { + builder.Append (' '); + + if ((flags[i] & SettableFlags) != 0) + builder.AppendFormat ("{0} ", ImapUtils.FormatFlagsList (flags[i], 0)); + + if (dates != null) + builder.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (dates[i])); + + //if (annotations != null && annotations[i] != null && annotations[i].Count > 0) { + // ImapUtils.FormatAnnotations (builder, annotations[i], list, false); + + // if (builder[builder.Length - 1] != ' ') + // builder.Append (' '); + //} + + builder.Append ("%L"); + list.Add (messages[i]); + } + + builder.Append ("\r\n"); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, options, command, args); + ic.Progress = progress; + + Engine.QueueCommand (ic); + + return ic; + } + + async Task> AppendAsync (FormatOptions options, IList messages, IList flags, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (messages.Count != flags.Count) + throw new ArgumentException ("The number of messages and the number of flags must be equal."); + + CheckState (false, false); + + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; + + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); + + if (messages.Count == 0) + return new UniqueId[0]; + + if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { + var ic = QueueMultiAppend (format, messages, flags, null, null, cancellationToken, progress); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("APPEND", ic); + + var append = (AppendUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.AppendUid); + + if (append != null) + return append.UidSet; + + return new UniqueId[0]; + } + + // FIXME: use an aggregate progress reporter + var uids = new List (); + + for (int i = 0; i < messages.Count; i++) { + var uid = await AppendAsync (format, messages[i], flags[i], null, null, doAsync, cancellationToken, progress).ConfigureAwait (false); + if (uids != null && uid.HasValue) + uids.Add (uid.Value); + else + uids = null; + } + + if (uids == null) + return new UniqueId[0]; + + return uids; + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Append (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, messages, flags, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages does not match the number of flags. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AppendAsync (FormatOptions options, IList messages, IList flags, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, messages, flags, true, cancellationToken, progress); + } + + async Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (messages == null) + throw new ArgumentNullException (nameof (messages)); + + for (int i = 0; i < messages.Count; i++) { + if (messages[i] == null) + throw new ArgumentException ("One or more of the messages is null."); + } + + if (flags == null) + throw new ArgumentNullException (nameof (flags)); + + if (dates == null) + throw new ArgumentNullException (nameof (dates)); + + if (messages.Count != flags.Count || messages.Count != dates.Count) + throw new ArgumentException ("The number of messages, the number of flags, and the number of dates must be equal."); + + CheckState (false, false); + + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; + + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); + + if (messages.Count == 0) + return new UniqueId[0]; + + if ((Engine.Capabilities & ImapCapabilities.MultiAppend) != 0) { + var ic = QueueMultiAppend (format, messages, flags, dates, null, cancellationToken, progress); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("APPEND", ic); + + var append = (AppendUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.AppendUid); + + if (append != null) + return append.UidSet; + + return new UniqueId[0]; + } + + // FIXME: use an aggregate progress reporter + var uids = new List (); + + for (int i = 0; i < messages.Count; i++) { + var uid = await AppendAsync (format, messages[i], flags[i], dates[i], null, doAsync, cancellationToken, progress).ConfigureAwait (false); + if (uids != null && uid.HasValue) + uids.Add (uid.Value); + else + uids = null; + } + + if (uids == null) + return new UniqueId[0]; + + return uids; + } + + /// + /// Append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Append (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, messages, flags, dates, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously append the specified messages to the folder. + /// + /// + /// Appends the specified messages to the folder and returns the UniqueIds assigned to the messages. + /// + /// The UIDs of the appended messages, if available; otherwise an empty array. + /// The formatting options. + /// The list of messages to append to the folder. + /// The message flags to use for each of the messages. + /// The received dates to use for each of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is null. + /// -or- + /// The number of messages, flags, and dates do not match. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AppendAsync (FormatOptions options, IList messages, IList flags, IList dates, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return AppendAsync (options, messages, flags, dates, true, cancellationToken, progress); + } + + ImapCommand QueueReplace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken, ITransferProgress progress) + { + var builder = new StringBuilder ($"UID REPLACE {uid} %F "); + var list = new List (); + + list.Add (this); + + if ((flags & SettableFlags) != 0) + builder.AppendFormat ("{0} ", ImapUtils.FormatFlagsList (flags, 0)); + + if (date.HasValue) + builder.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (date.Value)); + + //if (annotations != null && annotations.Count > 0) { + // ImapUtils.FormatAnnotations (builder, annotations, list, false); + // + // if (builder[builder.Length - 1] != ' ') + // builder.Append (' '); + //} + + builder.Append ("%L\r\n"); + list.Add (message); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, options, command, args); + ic.Progress = progress; + + Engine.QueueCommand (ic); + + return ic; + } + + async Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + CheckState (true, true); + + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var appended = await AppendAsync (options, message, flags, date, annotations, doAsync, cancellationToken, progress).ConfigureAwait (false); + await ModifyFlagsAsync (new[] { uid }, null, MessageFlags.Deleted, null, "+FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + if ((Engine.Capabilities & ImapCapabilities.UidPlus) != 0) + await ExpungeAsync (new[] { uid }, doAsync, cancellationToken).ConfigureAwait (false); + return appended; + } + + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; + + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); + + var ic = QueueReplace (format, uid, message, flags, date, annotations, cancellationToken, progress); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("REPLACE", ic); + + var append = (AppendUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.AppendUid); + + if (append != null) + return append.UidSet[0]; + + return null; + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, uid, message, flags, null, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, uid, message, flags, null, null, true, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Replace (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, uid, message, flags, date, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The UID of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ReplaceAsync (FormatOptions options, UniqueId uid, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, uid, message, flags, date, null, true, cancellationToken, progress); + } + + ImapCommand QueueReplace (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, CancellationToken cancellationToken, ITransferProgress progress) + { + var builder = new StringBuilder ($"REPLACE %d %F "); + var list = new List (); + + list.Add (index + 1); + list.Add (this); + + if ((flags & SettableFlags) != 0) + builder.AppendFormat ("{0} ", ImapUtils.FormatFlagsList (flags, 0)); + + if (date.HasValue) + builder.AppendFormat ("\"{0}\" ", ImapUtils.FormatInternalDate (date.Value)); + + //if (annotations != null && annotations.Count > 0) { + // ImapUtils.FormatAnnotations (builder, annotations, list, false); + // + // if (builder[builder.Length - 1] != ' ') + // builder.Append (' '); + //} + + builder.Append ("%L\r\n"); + list.Add (message); + + var command = builder.ToString (); + var args = list.ToArray (); + + var ic = new ImapCommand (Engine, cancellationToken, null, options, command, args); + ic.Progress = progress; + + Engine.QueueCommand (ic); + + return ic; + } + + async Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset? date, IList annotations, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + CheckState (true, true); + + if ((Engine.Capabilities & ImapCapabilities.Replace) == 0) { + var uid = await AppendAsync (options, message, flags, date, annotations, doAsync, cancellationToken, progress).ConfigureAwait (false); + await ModifyFlagsAsync (new[] { index }, null, MessageFlags.Deleted, null, "+FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + return uid; + } + + if (options.International && (Engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) + throw new NotSupportedException ("The IMAP server does not support the UTF8 extension."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if ((Engine.Capabilities & ImapCapabilities.UTF8Only) == ImapCapabilities.UTF8Only) + format.International = true; + + if (format.International && !Engine.UTF8Enabled) + throw new InvalidOperationException ("The UTF8 extension has not been enabled."); + + var ic = QueueReplace (format, index, message, flags, date, annotations, cancellationToken, progress); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, this); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("REPLACE", ic); + + var append = (AppendUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.AppendUid); + + if (append != null) + return append.UidSet[0]; + + return null; + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, index, message, flags, null, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags = MessageFlags.None, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, index, message, flags, null, null, true, cancellationToken, progress); + } + + /// + /// Replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueId? Replace (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, index, message, flags, date, null, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously replace a message in the folder. + /// + /// + /// Replaces the specified message in the folder and returns the UniqueId assigned to the new message. + /// + /// The UID of the new message, if available; otherwise, null. + /// The formatting options. + /// The index of the message to be replaced. + /// The message. + /// The message flags. + /// The received date of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// Internationalized formatting was requested but has not been enabled. + /// + /// + /// The does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task ReplaceAsync (FormatOptions options, int index, MimeMessage message, MessageFlags flags, DateTimeOffset date, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return ReplaceAsync (options, index, message, flags, date, null, true, cancellationToken, progress); + } + + async Task> GetIndexesAsync (IList uids, bool doAsync, CancellationToken cancellationToken) + { + var command = string.Format ("SEARCH UID {0}\r\n", UniqueIdSet.ToString (uids)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var results = new SearchResults (SortOrder.Ascending); + + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + + ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.UserData = results; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SEARCH", ic); + + var indexes = new int[results.UniqueIds.Count]; + for (int i = 0; i < indexes.Length; i++) + indexes[i] = (int) results.UniqueIds[i].Id - 1; + + return indexes; + } + + async Task CopyToAsync (IList uids, IMailFolder destination, bool doAsync, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); + + CheckState (true, false); + + if (uids.Count == 0) + return UniqueIdMap.Empty; + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = await GetIndexesAsync (uids, doAsync, cancellationToken).ConfigureAwait (false); + await CopyToAsync (indexes, destination, doAsync, cancellationToken).ConfigureAwait (false); + return UniqueIdMap.Empty; + } + + UniqueIdSet dest = null; + UniqueIdSet src = null; + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID COPY %s %F\r\n", uids, destination)) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, destination); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("COPY", ic); + + var copy = (CopyUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.CopyUid); + + if (copy != null) { + if (dest == null) { + dest = copy.DestUidSet; + src = copy.SrcUidSet; + } else { + dest.AddRange (copy.DestUidSet); + src.AddRange (copy.SrcUidSet); + } + } + } + + if (dest == null) + return UniqueIdMap.Empty; + + return new UniqueIdMap (src, dest); + } + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueIdMap CopyTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return CopyToAsync (uids, destination, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server does not support the UIDPLUS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CopyToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return CopyToAsync (uids, destination, true, cancellationToken); + } + + async Task MoveToAsync (IList uids, IMailFolder destination, bool doAsync, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + var copied = await CopyToAsync (uids, destination, doAsync, cancellationToken).ConfigureAwait (false); + await ModifyFlagsAsync (uids, null, MessageFlags.Deleted, null, "+FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + await ExpungeAsync (uids, doAsync, cancellationToken).ConfigureAwait (false); + return copied; + } + + if ((Engine.Capabilities & ImapCapabilities.UidPlus) == 0) { + var indexes = await GetIndexesAsync (uids, doAsync, cancellationToken).ConfigureAwait (false); + await MoveToAsync (indexes, destination, doAsync, cancellationToken).ConfigureAwait (false); + return UniqueIdMap.Empty; + } + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); + + CheckState (true, true); + + if (uids.Count == 0) + return UniqueIdMap.Empty; + + UniqueIdSet dest = null; + UniqueIdSet src = null; + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, "UID MOVE %s %F\r\n", uids, destination)) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, destination); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("MOVE", ic); + + var copy = (CopyUidResponseCode) GetResponseCode (ic, ImapResponseCodeType.CopyUid); + + if (copy != null) { + if (dest == null) { + dest = copy.DestUidSet; + src = copy.SrcUidSet; + } else { + dest.AddRange (copy.DestUidSet); + src.AddRange (copy.SrcUidSet); + } + } + } + + if (dest == null) + return UniqueIdMap.Empty; + + return new UniqueIdMap (src, dest); + } + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// If the IMAP server supports the MOVE extension (check the + /// property for the flag), then this operation will be atomic. + /// Otherwise, MailKit implements this by first copying the messages to the destination folder, then + /// marking them for deletion in the originating folder, and finally expunging them (see + /// for more information about how a + /// subset of messages are expunged). Since the server could disconnect at any point between those 3 + /// (or more) commands, it is advisable for clients to implement their own logic for moving messages when + /// the IMAP server does not support the MOVE command in order to better handle spontanious server + /// disconnects and other error conditions. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override UniqueIdMap MoveTo (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return MoveToAsync (uids, destination, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// Moves the specified messages to the destination folder. + /// If the IMAP server supports the MOVE extension (check the + /// property for the flag), then this operation will be atomic. + /// Otherwise, MailKit implements this by first copying the messages to the destination folder, then + /// marking them for deletion in the originating folder, and finally expunging them (see + /// for more information about how a + /// subset of messages are expunged). Since the server could disconnect at any point between those 3 + /// (or more) commands, it is advisable for clients to implement their own logic for moving messages when + /// the IMAP server does not support the MOVE command in order to better handle spontanious server + /// disconnects and other error conditions. + /// + /// The UID mapping of the messages in the destination folder, if available; otherwise an empty mapping. + /// The UIDs of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task MoveToAsync (IList uids, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return MoveToAsync (uids, destination, true, cancellationToken); + } + + async Task CopyToAsync (IList indexes, IMailFolder destination, bool doAsync, CancellationToken cancellationToken) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return; + + var set = ImapUtils.FormatIndexSet (indexes); + var command = string.Format ("COPY {0} %F\r\n", set); + var ic = Engine.QueueCommand (cancellationToken, this, command, destination); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, destination); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("COPY", ic); + } + + /// + /// Copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void CopyTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + CopyToAsync (indexes, destination, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously copy the specified messages to the destination folder. + /// + /// + /// Copies the specified messages to the destination folder. + /// + /// An awaitable task. + /// The indexes of the messages to copy. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task CopyToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return CopyToAsync (indexes, destination, true, cancellationToken); + } + + async Task MoveToAsync (IList indexes, IMailFolder destination, bool doAsync, CancellationToken cancellationToken) + { + if ((Engine.Capabilities & ImapCapabilities.Move) == 0) { + await CopyToAsync (indexes, destination, doAsync, cancellationToken).ConfigureAwait (false); + await ModifyFlagsAsync (indexes, null, MessageFlags.Deleted, null, "+FLAGS.SILENT", doAsync, cancellationToken).ConfigureAwait (false); + return; + } + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + if (!(destination is ImapFolder) || ((ImapFolder) destination).Engine != Engine) + throw new ArgumentException ("The destination folder does not belong to this ImapClient.", nameof (destination)); + + CheckState (true, true); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return; + + var set = ImapUtils.FormatIndexSet (indexes); + var command = string.Format ("MOVE {0} %F\r\n", set); + var ic = Engine.QueueCommand (cancellationToken, this, command, destination); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, destination); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("MOVE", ic); + } + + /// + /// Move the specified messages to the destination folder. + /// + /// + /// If the IMAP server supports the MOVE command, then the MOVE command will be used. Otherwise, + /// the messages will first be copied to the destination folder and then marked as \Deleted in the + /// originating folder. Since the server could disconnect at any point between those 2 operations, it + /// may be advisable to implement your own logic for moving messages in this case in order to better + /// handle spontanious server disconnects and other error conditions. + /// + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void MoveTo (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + MoveToAsync (indexes, destination, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously move the specified messages to the destination folder. + /// + /// + /// If the IMAP server supports the MOVE command, then the MOVE command will be used. Otherwise, + /// the messages will first be copied to the destination folder and then marked as \Deleted in the + /// originating folder. Since the server could disconnect at any point between those 2 operations, it + /// may be advisable to implement your own logic for moving messages in this case in order to better + /// handle spontanious server disconnects and other error conditions. + /// + /// An awaitable task. + /// The indexes of the messages to move. + /// The destination folder. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// The destination folder does not belong to the . + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// does not exist. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task MoveToAsync (IList indexes, IMailFolder destination, CancellationToken cancellationToken = default (CancellationToken)) + { + return MoveToAsync (indexes, destination, true, cancellationToken); + } + + #region IEnumerable implementation + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + public override IEnumerator GetEnumerator () + { + CheckState (true, false); + + for (int i = 0; i < Count; i++) + yield return GetMessage (i, CancellationToken.None); + + yield break; + } + + #endregion + + #region Untagged response handlers called by ImapEngine + + internal void OnExists (int count) + { + if (Count == count) + return; + + Count = count; + + OnCountChanged (); + } + + internal void OnExpunge (int index) + { + Count--; + + OnMessageExpunged (new MessageEventArgs (index)); + OnCountChanged (); + } + + internal async Task OnFetchAsync (ImapEngine engine, int index, bool doAsync, CancellationToken cancellationToken) + { + var message = new MessageSummary (this, index); + UniqueId? uid = null; + + await FetchSummaryItemsAsync (engine, message, doAsync, cancellationToken).ConfigureAwait (false); + + if ((message.Fields & MessageSummaryItems.UniqueId) != 0) + uid = message.UniqueId; + + if ((message.Fields & MessageSummaryItems.Flags) != 0) { + var args = new MessageFlagsChangedEventArgs (index, message.Flags.Value, message.Keywords); + args.ModSeq = message.ModSeq; + args.UniqueId = uid; + + OnMessageFlagsChanged (args); + } + + if ((message.Fields & MessageSummaryItems.GMailLabels) != 0) { + var args = new MessageLabelsChangedEventArgs (index, message.GMailLabels); + args.ModSeq = message.ModSeq; + args.UniqueId = uid; + + OnMessageLabelsChanged (args); + } + + if ((message.Fields & MessageSummaryItems.Annotations) != 0) { + var args = new AnnotationsChangedEventArgs (index, message.Annotations); + args.ModSeq = message.ModSeq; + args.UniqueId = uid; + + OnAnnotationsChanged (args); + } + + if ((message.Fields & MessageSummaryItems.ModSeq) != 0) { + var args = new ModSeqChangedEventArgs (index, message.ModSeq.Value); + args.UniqueId = uid; + + OnModSeqChanged (args); + } + + if (message.Fields != MessageSummaryItems.None) + OnMessageSummaryFetched (message); + } + + internal void OnRecent (int count) + { + if (Recent == count) + return; + + Recent = count; + + OnRecentChanged (); + } + + internal async Task OnVanishedAsync (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + UniqueIdSet vanished; + bool earlier = false; + + if (token.Type == ImapTokenType.OpenParen) { + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + + var atom = (string) token.Value; + + if (atom == "EARLIER") + earlier = true; + } while (true); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + vanished = ImapEngine.ParseUidSet (token, UidValidity, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "VANISHED", token); + + OnMessagesVanished (new MessagesVanishedEventArgs (vanished, earlier)); + + if (!earlier) { + Count -= vanished.Count; + + OnCountChanged (); + } + } + + internal void UpdateAttributes (FolderAttributes attrs) + { + var unsubscribed = false; + var subscribed = false; + + if ((attrs & FolderAttributes.Subscribed) == 0) + unsubscribed = (Attributes & FolderAttributes.Subscribed) != 0; + else + subscribed = (Attributes & FolderAttributes.Subscribed) == 0; + + var deleted = ((attrs & FolderAttributes.NonExistent) != 0) && + (Attributes & FolderAttributes.NonExistent) == 0; + + Attributes = attrs; + + if (unsubscribed) + OnUnsubscribed (); + + if (subscribed) + OnSubscribed (); + + if (deleted) + OnDeleted (); + } + + internal void UpdateAcceptedFlags (MessageFlags flags) + { + AcceptedFlags = flags; + } + + internal void UpdatePermanentFlags (MessageFlags flags) + { + PermanentFlags = flags; + } + + internal void UpdateIsNamespace (bool value) + { + IsNamespace = value; + } + + internal void UpdateUnread (int count) + { + if (Unread == count) + return; + + Unread = count; + + OnUnreadChanged (); + } + + internal void UpdateUidNext (UniqueId uid) + { + if (UidNext.HasValue && UidNext.Value == uid) + return; + + UidNext = uid; + + OnUidNextChanged (); + } + + internal void UpdateAppendLimit (uint? limit) + { + AppendLimit = limit; + } + + internal void UpdateSize (ulong? size) + { + if (Size == size) + return; + + Size = size; + + OnSizeChanged (); + } + + internal void UpdateId (string id) + { + if (Id == id) + return; + + Id = id; + + OnIdChanged (); + } + + internal void UpdateHighestModSeq (ulong modseq) + { + if (HighestModSeq == modseq) + return; + + HighestModSeq = modseq; + + OnHighestModSeqChanged (); + } + + internal void UpdateUidValidity (uint validity) + { + if (UidValidity == validity) + return; + + UidValidity = validity; + + OnUidValidityChanged (); + } + + internal void OnRenamed (ImapFolderConstructorArgs args) + { + var oldFullName = FullName; + + InitializeProperties (args); + + OnRenamed (oldFullName, FullName); + } + + #endregion + + #endregion + } +} diff --git a/src/MailKit/Net/Imap/ImapFolderAnnotations.cs b/src/MailKit/Net/Imap/ImapFolderAnnotations.cs new file mode 100644 index 0000000..cf20979 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolderAnnotations.cs @@ -0,0 +1,567 @@ +// +// ImapFolderAnnotations.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + async Task> StoreAsync (IList uids, ulong? modseq, IList annotations, bool doAsync, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + CheckState (true, true); + + if (AnnotationAccess == AnnotationAccess.None) + throw new NotSupportedException ("The ImapFolder does not support annotations."); + + if (uids.Count == 0 || annotations.Count == 0) + return new UniqueId[0]; + + var builder = new StringBuilder ("UID STORE %s "); + var values = new List (); + UniqueIdSet unmodified = null; + + if (modseq.HasValue) + builder.AppendFormat (CultureInfo.InvariantCulture, "(UNCHANGEDSINCE {0}) ", modseq.Value); + + ImapUtils.FormatAnnotations (builder, annotations, values, true); + builder.Append ("\r\n"); + + var command = builder.ToString (); + var args = values.ToArray (); + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, command, uids, args)) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + ProcessUnmodified (ic, ref unmodified, modseq); + } + + if (unmodified == null) + return new UniqueId[0]; + + return unmodified; + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Store (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + StoreAsync (uids, null, annotations, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task StoreAsync (IList uids, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (uids, null, annotations, true, cancellationToken); + } + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (uids, modseq, annotations, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> StoreAsync (IList uids, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (uids, modseq, annotations, true, cancellationToken); + } + + async Task> StoreAsync (IList indexes, ulong? modseq, IList annotations, bool doAsync, CancellationToken cancellationToken) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + if (annotations == null) + throw new ArgumentNullException (nameof (annotations)); + + CheckState (true, true); + + if (AnnotationAccess == AnnotationAccess.None) + throw new NotSupportedException ("The ImapFolder does not support annotations."); + + if (indexes.Count == 0 || annotations.Count == 0) + return new int[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var builder = new StringBuilder ("STORE "); + var values = new List (); + + builder.AppendFormat ("{0} ", set); + + if (modseq.HasValue) + builder.AppendFormat (CultureInfo.InvariantCulture, "(UNCHANGEDSINCE {0}) ", modseq.Value); + + ImapUtils.FormatAnnotations (builder, annotations, values, true); + builder.Append ("\r\n"); + + var command = builder.ToString (); + var args = values.ToArray (); + + var ic = Engine.QueueCommand (cancellationToken, this, command, args); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + return GetUnmodified (ic, modseq); + } + + /// + /// Store the annotations for the specified messages. + /// + /// + /// Stores the annotations for the specified messages. + /// + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void Store (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + StoreAsync (indexes, null, annotations, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously store the annotations for the specified messages. + /// + /// + /// Asynchronously stores the annotations for the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task StoreAsync (IList indexes, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (indexes, null, annotations, true, cancellationToken); + } + + /// + /// Store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Stores the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Store (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (indexes, modseq, annotations, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously store the annotations for the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Asynchronously stores the annotations for the specified messages only if their mod-sequence value is less than the specified value.s + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The annotations to store. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// Cannot store annotations without any properties defined. + /// + /// + /// The does not support annotations. + /// -or- + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> StoreAsync (IList indexes, ulong modseq, IList annotations, CancellationToken cancellationToken = default (CancellationToken)) + { + return StoreAsync (indexes, modseq, annotations, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapFolderConstructorArgs.cs b/src/MailKit/Net/Imap/ImapFolderConstructorArgs.cs new file mode 100644 index 0000000..c9ba6aa --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolderConstructorArgs.cs @@ -0,0 +1,113 @@ +// +// ImapFolderInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net.Imap { + /// + /// Constructor arguments for . + /// + /// + /// Constructor arguments for . + /// + public sealed class ImapFolderConstructorArgs + { + internal readonly string EncodedName; + internal readonly ImapEngine Engine; + + /// + /// Initializes a new instance of the class. + /// + /// The IMAP command engine. + /// The encoded name. + /// The attributes. + /// The directory separator. + internal ImapFolderConstructorArgs (ImapEngine engine, string encodedName, FolderAttributes attributes, char delim) : this () + { + FullName = engine.DecodeMailboxName (encodedName); + Name = GetBaseName (FullName, delim); + DirectorySeparator = delim; + EncodedName = encodedName; + Attributes = attributes; + Engine = engine; + } + + ImapFolderConstructorArgs () + { + } + + /// + /// Get the folder attributes. + /// + /// + /// Gets the folder attributes. + /// + /// The folder attributes. + public FolderAttributes Attributes { + get; private set; + } + + /// + /// Get the directory separator. + /// + /// + /// Gets the directory separator. + /// + /// The directory separator. + public char DirectorySeparator { + get; private set; + } + + /// + /// Get the full name of the folder. + /// + /// + /// This is the equivalent of the full path of a file on a file system. + /// + /// The full name of the folder. + public string FullName { + get; private set; + } + + /// + /// Get the name of the folder. + /// + /// + /// This is the equivalent of the file name of a file on the file system. + /// + /// The name of the folder. + public string Name { + get; private set; + } + + static string GetBaseName (string fullName, char delim) + { + var names = fullName.Split (new [] { delim }, StringSplitOptions.RemoveEmptyEntries); + + return names.Length > 0 ? names[names.Length - 1] : fullName; + } + } +} diff --git a/src/MailKit/Net/Imap/ImapFolderFetch.cs b/src/MailKit/Net/Imap/ImapFolderFetch.cs new file mode 100644 index 0000000..d08e9c8 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolderFetch.cs @@ -0,0 +1,6770 @@ +// +// ImapFolderFetch.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +using MimeKit; +using MimeKit.IO; +using MimeKit.Text; +using MimeKit.Utils; + +using MailKit.Search; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + internal static readonly HashSet EmptyHeaderFields = new HashSet (); + const int PreviewHtmlLength = 16 * 1024; + const int PreviewTextLength = 512; + + class FetchSummaryContext + { + public readonly List Messages; + + public FetchSummaryContext () + { + Messages = new List (); + } + + int BinarySearch (int index, bool insert) + { + int min = 0, max = Messages.Count; + + if (max == 0) + return insert ? 0 : -1; + + do { + int i = min + ((max - min) / 2); + + if (index == Messages[i].Index) + return i; + + if (index > Messages[i].Index) { + min = i + 1; + } else { + max = i; + } + } while (min < max); + + return insert ? min : -1; + } + + public void Add (int index, MessageSummary message) + { + int i = BinarySearch (index, true); + + if (i < Messages.Count) + Messages.Insert (i, message); + else + Messages.Add (message); + } + + public bool TryGetValue (int index, out MessageSummary message) + { + int i; + + if ((i = BinarySearch (index, false)) == -1) { + message = null; + return false; + } + + message = (MessageSummary) Messages[i]; + + return true; + } + + public void OnMessageExpunged (object sender, MessageEventArgs args) + { + int index = BinarySearch (args.Index, true); + + if (index >= Messages.Count) + return; + + if (Messages[index].Index == args.Index) + Messages.RemoveAt (index); + + for (int i = index; i < Messages.Count; i++) { + var message = (MessageSummary) Messages[i]; + message.Index--; + } + } + } + + static async Task ReadLiteralDataAsync (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + var buf = new byte[4096]; + int nread; + + do { + if (doAsync) + nread = await engine.Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false); + else + nread = engine.Stream.Read (buf, 0, buf.Length, cancellationToken); + } while (nread > 0); + } + + static async Task SkipParenthesizedList (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + do { + var token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + return; + + // token is safe to read, so pop it off the queue + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + if (token.Type == ImapTokenType.OpenParen) { + // skip the inner parenthesized list + await SkipParenthesizedList (engine, doAsync, cancellationToken).ConfigureAwait (false); + } + } while (true); + } + + async Task FetchSummaryItemsAsync (ImapEngine engine, MessageSummary message, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen || token.Type == ImapTokenType.Eoln) + break; + + bool parenthesized = false; + if (engine.QuirksMode == ImapQuirksMode.Domino && token.Type == ImapTokenType.OpenParen) { + // Note: Lotus Domino IMAP will (sometimes?) encapsulate the `ENVELOPE` segment of the + // response within an extra set of parenthesis. + // + // See https://github.com/jstedfast/MailKit/issues/943 for details. + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + parenthesized = true; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + string format; + ulong value64; + uint value; + int idx; + + switch (atom.ToUpperInvariant ()) { + case "INTERNALDATE": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.QString: + case ImapTokenType.Atom: + message.InternalDate = ImapUtils.ParseInternalDate ((string) token.Value); + break; + case ImapTokenType.Nil: + message.InternalDate = null; + break; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + message.Fields |= MessageSummaryItems.InternalDate; + break; + case "RFC822.SIZE": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + message.Size = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.Size; + break; + case "BODYSTRUCTURE": + format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "BODYSTRUCTURE", "{0}"); + message.Body = await ImapUtils.ParseBodyAsync (engine, format, string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.BodyStructure; + break; + case "BODY": + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + format = ImapEngine.FetchBodySyntaxErrorFormat; + + if (token.Type == ImapTokenType.OpenBracket) { + var referencesField = false; + var headerFields = false; + + // consume the '[' + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, format, token); + + // References and/or other headers were requested... + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + engine.Stream.UngetToken (token); + + var field = await ImapUtils.ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (headerFields && !referencesField && field.Equals ("REFERENCES", StringComparison.OrdinalIgnoreCase)) + referencesField = true; + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + atom = (string) token.Value; + + headerFields = atom.Equals ("HEADER.FIELDS", StringComparison.OrdinalIgnoreCase); + + if (!headerFields && atom.Equals ("HEADER", StringComparison.OrdinalIgnoreCase)) { + // if we're fetching *all* headers, then it will include the References header (if it exists) + referencesField = true; + } + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, format, token); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Literal, format, token); + + try { + message.Headers = await engine.ParseHeadersAsync (engine.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } catch (FormatException) { + message.Headers = new HeaderList (); + } + + // consume any remaining literal data... (typically extra blank lines) + await ReadLiteralDataAsync (engine, doAsync, cancellationToken).ConfigureAwait (false); + + message.References = new MessageIdList (); + + if ((idx = message.Headers.IndexOf (HeaderId.References)) != -1) { + var references = message.Headers[idx]; + var rawValue = references.RawValue; + + foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) + message.References.Add (msgid); + } + + message.Fields |= MessageSummaryItems.Headers; + + if (referencesField) + message.Fields |= MessageSummaryItems.References; + } else { + message.Body = await ImapUtils.ParseBodyAsync (engine, format, string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Body; + } + break; + case "ENVELOPE": + message.Envelope = await ImapUtils.ParseEnvelopeAsync (engine, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Envelope; + break; + case "FLAGS": + message.Flags = await ImapUtils.ParseFlagsListAsync (engine, atom, message.Keywords, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Flags; + break; + case "MODSEQ": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + value64 = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ModSeq; + message.ModSeq = value64; + + if (value64 > HighestModSeq) + UpdateHighestModSeq (value64); + break; + case "UID": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.UniqueId = new UniqueId (UidValidity, value); + message.Fields |= MessageSummaryItems.UniqueId; + break; + case "EMAILID": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.EmailId; + message.EmailId = (string) token.Value; + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + break; + case "THREADID": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = (string) token.Value; + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Nil, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + message.Fields |= MessageSummaryItems.ThreadId; + message.ThreadId = null; + } + break; + case "X-GM-MSGID": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailMessageId; + message.GMailMessageId = value64; + break; + case "X-GM-THRID": + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + value64 = ImapEngine.ParseNumber64 (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + message.Fields |= MessageSummaryItems.GMailThreadId; + message.GMailThreadId = value64; + break; + case "X-GM-LABELS": + message.GMailLabels = await ImapUtils.ParseLabelsListAsync (engine, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.GMailLabels; + break; + case "ANNOTATION": + message.Annotations = await ImapUtils.ParseAnnotationsAsync (engine, doAsync, cancellationToken).ConfigureAwait (false); + message.Fields |= MessageSummaryItems.Annotations; + break; + default: + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) + await SkipParenthesizedList (engine, doAsync, cancellationToken).ConfigureAwait (false); + break; + } + + if (parenthesized) { + // Note: This is the second half of the Lotus Domino IMAP server work-around. + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + } + + async Task FetchSummaryItemsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var ctx = (FetchSummaryContext) ic.UserData; + MessageSummary message; + + if (!ctx.TryGetValue (index, out message)) { + message = new MessageSummary (this, index); + ctx.Add (index, message); + } + + await FetchSummaryItemsAsync (engine, message, doAsync, ic.CancellationToken).ConfigureAwait (false); + + OnMessageSummaryFetched (message); + } + + internal static string FormatSummaryItems (ImapEngine engine, ref MessageSummaryItems items, HashSet headers, out bool previewText, bool isNotify = false) + { + if ((items & MessageSummaryItems.PreviewText) != 0) { + // if the user wants the preview text, we will also need the UIDs and BODYSTRUCTUREs + // so that we can request a preview of the body text in subsequent FETCH requests. + items |= MessageSummaryItems.BodyStructure | MessageSummaryItems.UniqueId; + previewText = true; + } else { + previewText = false; + } + + if ((items & MessageSummaryItems.BodyStructure) != 0 && (items & MessageSummaryItems.Body) != 0) { + // don't query both the BODY and BODYSTRUCTURE, that's just dumb... + items &= ~MessageSummaryItems.Body; + } + + if (engine.QuirksMode != ImapQuirksMode.GMail && !isNotify) { + // first, eliminate the aliases... + var alias = items & ~MessageSummaryItems.PreviewText; + + if (alias == MessageSummaryItems.All) + return "ALL"; + + if (alias == MessageSummaryItems.Full) + return "FULL"; + + if (alias == MessageSummaryItems.Fast) + return "FAST"; + } + + var tokens = new List (); + + // now add on any additional summary items... + if ((items & MessageSummaryItems.UniqueId) != 0) + tokens.Add ("UID"); + if ((items & MessageSummaryItems.Flags) != 0) + tokens.Add ("FLAGS"); + if ((items & MessageSummaryItems.InternalDate) != 0) + tokens.Add ("INTERNALDATE"); + if ((items & MessageSummaryItems.Size) != 0) + tokens.Add ("RFC822.SIZE"); + if ((items & MessageSummaryItems.Envelope) != 0) + tokens.Add ("ENVELOPE"); + if ((items & MessageSummaryItems.BodyStructure) != 0) + tokens.Add ("BODYSTRUCTURE"); + if ((items & MessageSummaryItems.Body) != 0) + tokens.Add ("BODY"); + + if ((engine.Capabilities & ImapCapabilities.CondStore) != 0) { + if ((items & MessageSummaryItems.ModSeq) != 0) + tokens.Add ("MODSEQ"); + } + + if ((engine.Capabilities & ImapCapabilities.Annotate) != 0) { + if ((items & MessageSummaryItems.Annotations) != 0) + tokens.Add ("ANNOTATION (/* (value size))"); + } + + if ((engine.Capabilities & ImapCapabilities.ObjectID) != 0) { + if ((items & MessageSummaryItems.EmailId) != 0) + tokens.Add ("EMAILID"); + if ((items & MessageSummaryItems.ThreadId) != 0) + tokens.Add ("THREADID"); + } + + if ((engine.Capabilities & ImapCapabilities.GMailExt1) != 0) { + // now for the GMail extension items + if ((items & MessageSummaryItems.GMailMessageId) != 0) + tokens.Add ("X-GM-MSGID"); + if ((items & MessageSummaryItems.GMailThreadId) != 0) + tokens.Add ("X-GM-THRID"); + if ((items & MessageSummaryItems.GMailLabels) != 0) + tokens.Add ("X-GM-LABELS"); + } + + if ((items & MessageSummaryItems.Headers) != 0) { + tokens.Add ("BODY.PEEK[HEADER]"); + } else if ((items & MessageSummaryItems.References) != 0 || headers.Count > 0) { + var headerFields = new StringBuilder ("BODY.PEEK[HEADER.FIELDS ("); + var references = false; + + foreach (var header in headers) { + if (header.Equals ("REFERENCES", StringComparison.OrdinalIgnoreCase)) + references = true; + + headerFields.Append (header); + headerFields.Append (' '); + } + + if ((items & MessageSummaryItems.References) != 0 && !references) + headerFields.Append ("REFERENCES "); + + headerFields[headerFields.Length - 1] = ')'; + headerFields.Append (']'); + + tokens.Add (headerFields.ToString ()); + } + + if (tokens.Count == 1 && !isNotify) + return tokens[0]; + + return string.Format ("({0})", string.Join (" ", tokens)); + } + + string FormatSummaryItems (ref MessageSummaryItems items, HashSet headers, out bool previewText) + { + return FormatSummaryItems (Engine, ref items, headers, out previewText); + } + + static IList AsReadOnly (ICollection collection) + { + var array = new IMessageSummary[collection.Count]; + + collection.CopyTo (array, 0); + + return new ReadOnlyCollection (array); + } + + class FetchPreviewTextContext : FetchStreamContextBase + { + static readonly PlainTextPreviewer textPreviewer = new PlainTextPreviewer (); + static readonly HtmlTextPreviewer htmlPreviewer = new HtmlTextPreviewer (); + + readonly FetchSummaryContext ctx; + readonly ImapFolder folder; + + public FetchPreviewTextContext (ImapFolder folder, FetchSummaryContext ctx) : base (null) + { + this.folder = folder; + this.ctx = ctx; + } + + public override Task AddAsync (Section section, bool doAsync, CancellationToken cancellationToken) + { + MessageSummary message; + + if (!ctx.TryGetValue (section.Index, out message)) + return Complete; + + var body = message.TextBody; + TextPreviewer previewer; + + if (body == null) { + previewer = htmlPreviewer; + body = message.HtmlBody; + } else { + previewer = textPreviewer; + } + + if (body == null) + return Complete; + + var charset = body.ContentType.Charset ?? "utf-8"; + ContentEncoding encoding; + + if (!string.IsNullOrEmpty (body.ContentTransferEncoding)) + MimeUtils.TryParse (body.ContentTransferEncoding, out encoding); + else + encoding = ContentEncoding.Default; + + using (var memory = new MemoryStream ()) { + var content = new MimeContent (section.Stream, encoding); + + content.DecodeTo (memory); + memory.Position = 0; + + try { + message.PreviewText = previewer.GetPreviewText (memory, charset); + } catch (DecoderFallbackException) { + memory.Position = 0; + + message.PreviewText = previewer.GetPreviewText (memory, ImapEngine.Latin1); + } + + message.Fields |= MessageSummaryItems.PreviewText; + folder.OnMessageSummaryFetched (message); + } + + return Complete; + } + + public override Task SetUniqueIdAsync (int index, UniqueId uid, bool doAsync, CancellationToken cancellationToken) + { + return Complete; + } + } + + async Task FetchPreviewTextAsync (FetchSummaryContext sctx, Dictionary bodies, int octets, bool doAsync, CancellationToken cancellationToken) + { + foreach (var pair in bodies) { + var uids = pair.Value; + string specifier; + + if (!string.IsNullOrEmpty (pair.Key)) + specifier = pair.Key; + else + specifier = "TEXT"; + + // TODO: if the IMAP server supports the CONVERT extension, we could possibly use the + // CONVERT command instead to decode *and* convert (html) into utf-8 plain text. + // + // e.g. "UID CONVERT {0} (\"text/plain\" (\"charset\" \"utf-8\")) BINARY[{1}]<0.{2}>\r\n" + // + // This would allow us to more accurately fetch X number of characters because we wouldn't + // need to guestimate accounting for base64/quoted-printable decoding. + + var command = string.Format (CultureInfo.InvariantCulture, "UID FETCH {0} (BODY.PEEK[{1}]<0.{2}>)\r\n", uids, specifier, octets); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchPreviewTextContext (this, sctx); + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } finally { + ctx.Dispose (); + } + } + } + + async Task GetPreviewTextAsync (FetchSummaryContext sctx, bool doAsync, CancellationToken cancellationToken) + { + var textBodies = new Dictionary (); + var htmlBodies = new Dictionary (); + + foreach (var item in sctx.Messages) { + Dictionary bodies; + var message = (MessageSummary) item; + var body = message.TextBody; + UniqueIdSet uids; + + if (body == null) { + body = message.HtmlBody; + bodies = htmlBodies; + } else { + bodies = textBodies; + } + + if (body == null) { + message.Fields |= MessageSummaryItems.PreviewText; + message.PreviewText = string.Empty; + OnMessageSummaryFetched (message); + continue; + } + + if (!bodies.TryGetValue (body.PartSpecifier, out uids)) { + uids = new UniqueIdSet (SortOrder.Ascending); + bodies.Add (body.PartSpecifier, uids); + } + + uids.Add (message.UniqueId); + } + + MessageExpunged += sctx.OnMessageExpunged; + + try { + await FetchPreviewTextAsync (sctx, textBodies, PreviewTextLength, doAsync, cancellationToken).ConfigureAwait (false); + await FetchPreviewTextAsync (sctx, htmlBodies, PreviewHtmlLength, doAsync, cancellationToken).ConfigureAwait (false); + } finally { + MessageExpunged -= sctx.OnMessageExpunged; + } + } + + async Task> FetchAsync (IList uids, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + CheckState (true, false); + + if (uids.Count == 0) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var command = string.Format ("UID FETCH %s {0}\r\n", query); + var ctx = new FetchSummaryContext (); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + CheckState (true, false); + + if (uids.Count == 0) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var command = string.Format ("UID FETCH %s {0}\r\n", query); + var ctx = new FetchSummaryContext (); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + + if (uids.Count == 0) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var vanished = Engine.QResyncEnabled ? " VANISHED" : string.Empty; + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("UID FETCH %s {0} (CHANGEDSINCE {1}{2})\r\n", query, modseqValue, vanished); + var ctx = new FetchSummaryContext (); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + + if (uids.Count == 0) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var vanished = Engine.QResyncEnabled ? " VANISHED" : string.Empty; + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("UID FETCH %s {0} (CHANGEDSINCE {1}{2})\r\n", query, modseqValue, vanished); + var ctx = new FetchSummaryContext (); + + MessageExpunged += ctx.OnMessageExpunged; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } + } finally { + MessageExpunged -= ctx.OnMessageExpunged; + } + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + /// + /// Fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// + /// + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not currently open. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (uids, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs. + /// + /// + /// Fetches the message summaries for the specified message UIDs. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, items, headers, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, modseq, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, modseq, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (uids, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, modseq, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message UIDs that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message UIDs that + /// have a higher mod-sequence value than the one specified. + /// If the IMAP server supports the QRESYNC extension and the application has + /// enabled this feature via , + /// then this method will emit events for messages + /// that have vanished since the specified mod-sequence value. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The UIDs. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList uids, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (uids, modseq, items, headers, true, cancellationToken); + } + + async Task> FetchAsync (IList indexes, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return new IMessageSummary[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var command = string.Format ("FETCH {0} {1}\r\n", set, query); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return new IMessageSummary[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var command = string.Format ("FETCH {0} {1}\r\n", set, query); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return new IMessageSummary[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", set, query, modseqValue); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return new IMessageSummary[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", set, query, modseqValue); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + /// + /// Fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (indexes, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes. + /// + /// + /// Fetches the message summaries for the specified message indexes. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, items, headers, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, modseq, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, modseq, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (indexes, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, modseq, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the specified message indexes that have a + /// higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the specified message indexes that + /// have a higher mod-sequence value than the one specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The indexes. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (IList indexes, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (indexes, modseq, items, headers, true, cancellationToken); + } + + static string GetFetchRange (int min, int max) + { + var minValue = (min + 1).ToString (CultureInfo.InvariantCulture); + + if (min == max) + return minValue; + + var maxValue = max != -1 ? (max + 1).ToString (CultureInfo.InvariantCulture) : "*"; + + return string.Format (CultureInfo.InvariantCulture, "{0}:{1}", minValue, maxValue); + } + + async Task> FetchAsync (int min, int max, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + CheckState (true, false); + CheckAllowIndexes (); + + if (min == Count) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var command = string.Format ("FETCH {0} {1}\r\n", GetFetchRange (min, max), query); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + CheckState (true, false); + CheckAllowIndexes (); + + if (min == Count) + return new IMessageSummary[0]; + + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var command = string.Format ("FETCH {0} {1}\r\n", GetFetchRange (min, max), query); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + if (items == MessageSummaryItems.None) + throw new ArgumentOutOfRangeException (nameof (items)); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + var query = FormatSummaryItems (ref items, EmptyHeaderFields, out previewText); + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", GetFetchRange (min, max), query, modseqValue); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + async Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, bool doAsync, CancellationToken cancellationToken) + { + bool previewText; + + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + var headerFields = ImapUtils.GetUniqueHeaders (headers); + + if (!supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, false); + CheckAllowIndexes (); + + var query = FormatSummaryItems (ref items, headerFields, out previewText); + var modseqValue = modseq.ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} {1} (CHANGEDSINCE {2})\r\n", GetFetchRange (min, max), query, modseqValue); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchSummaryContext (); + + ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (previewText) + await GetPreviewTextAsync (ctx, doAsync, cancellationToken).ConfigureAwait (false); + + return AsReadOnly (ctx.Messages); + } + + /// + /// Fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (min, max, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes, inclusive. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes, inclusive. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, items, headers, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, modseq, items, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// -or- + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, modseq, items, true, cancellationToken); + } + + /// + /// Fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return Fetch (min, max, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, modseq, items, ImapUtils.GetUniqueHeaders (headers), cancellationToken); + } + + /// + /// Fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Fetch (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, modseq, items, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously fetches the message summaries for the messages between the two indexes (inclusive) + /// that have a higher mod-sequence value than the one specified. + /// + /// + /// Fetches the message summaries for the messages between the two + /// indexes (inclusive) that have a higher mod-sequence value than the one + /// specified. + /// It should be noted that if another client has modified any message + /// in the folder, the IMAP server may choose to return information that was + /// not explicitly requested. It is therefore important to be prepared to + /// handle both additional fields on a for + /// messages that were requested as well as summaries for messages that were + /// not requested at all. + /// + /// An enumeration of summaries for the requested messages. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// The mod-sequence value. + /// The message summary items to fetch. + /// The desired header fields. + /// The cancellation token. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// One or more of the specified is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> FetchAsync (int min, int max, ulong modseq, MessageSummaryItems items, IEnumerable headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return FetchAsync (min, max, modseq, items, headers, true, cancellationToken); + } + + /// + /// Create a backing stream for use with the GetMessage, GetBodyPart, and GetStream methods. + /// + /// + /// Allows subclass implementations to override the type of stream + /// created for use with the GetMessage, GetBodyPart and GetStream methods. + /// This could be useful for subclass implementations that intend to implement + /// support for caching and/or for subclass implementations that want to use + /// temporary file streams instead of memory-based streams for larger amounts of + /// message data. + /// Subclasses that implement caching using this API should wait for + /// before adding the stream to their cache. + /// Streams returned by this method SHOULD clean up any allocated resources + /// such as deleting temporary files from the file system. + /// The will not be available for the various + /// GetMessage(), GetBodyPart() and GetStream() methods that take a message index rather + /// than a . It may also not be available if the IMAP server + /// response does not specify the UID value prior to sending the literal-string + /// token containing the message stream. + /// + /// + /// The stream. + /// The unique identifier of the message, if available. + /// The section of the message that is being fetched. + /// The starting offset of the message section being fetched. + /// The length of the stream being fetched, measured in bytes. + protected virtual Stream CreateStream (UniqueId? uid, string section, int offset, int length) + { + if (length > 4096) + return new MemoryBlockStream (); + + return new MemoryStream (length); + } + + /// + /// Commit a stream returned by . + /// + /// + /// Commits a stream returned by . + /// This method is called only after both the message data has successfully + /// been written to the stream returned by and a + /// has been obtained for the associated message. + /// For subclasses implementing caching, this method should be used for + /// committing the stream to their cache. + /// Subclass implementations may take advantage of the fact that + /// allows returning a new + /// reference if they move a file on the file system and wish to return a new + /// based on the new path, for example. + /// + /// + /// The stream. + /// The stream. + /// The unique identifier of the message. + /// The section of the message that the stream represents. + /// The starting offset of the message section. + /// The length of the stream, measured in bytes. + protected virtual Stream CommitStream (Stream stream, UniqueId uid, string section, int offset, int length) + { + return stream; + } + + async Task ParseHeadersAsync (Stream stream, bool doAsync, CancellationToken cancellationToken) + { + try { + return await Engine.ParseHeadersAsync (stream, doAsync, cancellationToken).ConfigureAwait (false); + } finally { + stream.Dispose (); + } + } + + async Task ParseMessageAsync (Stream stream, bool doAsync, CancellationToken cancellationToken) + { + bool dispose = !(stream is MemoryStream || stream is MemoryBlockStream); + + try { + return await Engine.ParseMessageAsync (stream, !dispose, doAsync, cancellationToken).ConfigureAwait (false); + } finally { + if (dispose) + stream.Dispose (); + } + } + + async Task ParseEntityAsync (Stream stream, bool dispose, bool doAsync, CancellationToken cancellationToken) + { + try { + return await Engine.ParseEntityAsync (stream, !dispose, doAsync, cancellationToken).ConfigureAwait (false); + } finally { + if (dispose) + stream.Dispose (); + } + } + + class Section + { + public int Index; + public UniqueId? UniqueId; + public Stream Stream; + public string Name; + public int Offset; + public int Length; + + public Section (Stream stream, int index, UniqueId? uid, string name, int offset, int length) + { + Stream = stream; + Offset = offset; + Length = length; + UniqueId = uid; + Index = index; + Name = name; + } + } + + abstract class FetchStreamContextBase : IDisposable + { + protected static readonly Task Complete = Task.FromResult (true); + public readonly List
Sections = new List
(); + readonly ITransferProgress progress; + + public FetchStreamContextBase (ITransferProgress progress) + { + this.progress = progress; + } + + public abstract Task AddAsync (Section section, bool doAsync, CancellationToken cancellationToken); + + public virtual bool Contains (int index, string specifier, out Section section) + { + section = null; + return false; + } + + public abstract Task SetUniqueIdAsync (int index, UniqueId uid, bool doAsync, CancellationToken cancellationToken); + + public void Report (long nread, long total) + { + if (progress == null) + return; + + progress.Report (nread, total); + } + + public void Dispose () + { + for (int i = 0; i < Sections.Count; i++) { + var section = Sections[i]; + + try { + section.Stream.Dispose (); + } catch (IOException) { + } + } + } + } + + class FetchStreamContext : FetchStreamContextBase + { + public FetchStreamContext (ITransferProgress progress) : base (progress) + { + } + + public override Task AddAsync (Section section, bool doAsync, CancellationToken cancellationToken) + { + Sections.Add (section); + return Complete; + } + + public bool TryGetSection (UniqueId uid, string specifier, out Section section, bool remove = false) + { + for (int i = 0; i < Sections.Count; i++) { + var item = Sections[i]; + + if (!item.UniqueId.HasValue || item.UniqueId.Value != uid) + continue; + + if (item.Name.Equals (specifier, StringComparison.OrdinalIgnoreCase)) { + if (remove) + Sections.RemoveAt (i); + + section = item; + return true; + } + } + + section = null; + + return false; + } + + public bool TryGetSection (int index, string specifier, out Section section, bool remove = false) + { + for (int i = 0; i < Sections.Count; i++) { + var item = Sections[i]; + + if (item.Index != index) + continue; + + if (item.Name.Equals (specifier, StringComparison.OrdinalIgnoreCase)) { + if (remove) + Sections.RemoveAt (i); + + section = item; + return true; + } + } + + section = null; + + return false; + } + + public override Task SetUniqueIdAsync (int index, UniqueId uid, bool doAsync, CancellationToken cancellationToken) + { + for (int i = 0; i < Sections.Count; i++) { + if (Sections[i].Index == index) + Sections[i].UniqueId = uid; + } + + return Complete; + } + } + + async Task FetchStreamAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + var annotations = new AnnotationsChangedEventArgs (index); + var labels = new MessageLabelsChangedEventArgs (index); + var flags = new MessageFlagsChangedEventArgs (index); + var modSeq = new ModSeqChangedEventArgs (index); + var ctx = (FetchStreamContextBase) ic.UserData; + var sectionBuilder = new StringBuilder (); + bool annotationsChanged = false; + bool modSeqChanged = false; + bool labelsChanged = false; + bool flagsChanged = false; + var buf = new byte[4096]; + long nread = 0, size = 0; + UniqueId? uid = null; + Section section; + Stream stream; + string name; + int n; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) { + // Note: Most likely the the message body was calculated to be 1 or 2 bytes too + // short (e.g. did not include the trailing ) and that is the EOLN we just + // reached. Ignore it and continue as normal. + // + // See https://github.com/jstedfast/MailKit/issues/954 for details. + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + var atom = (string) token.Value; + int offset = 0, length; + ulong modseq; + uint value; + + switch (atom.ToUpperInvariant ()) { + case "BODY": + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Clear (); + + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseBracket) + break; + + if (token.Type == ImapTokenType.OpenParen) { + sectionBuilder.Append (" ("); + + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // the header field names will generally be atoms or qstrings but may also be literals + switch (token.Type) { + case ImapTokenType.Literal: + sectionBuilder.Append (await engine.ReadLiteralAsync (doAsync, ic.CancellationToken).ConfigureAwait (false)); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + sectionBuilder.Append ((string) token.Value); + break; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + sectionBuilder.Append (' '); + } while (true); + + if (sectionBuilder[sectionBuilder.Length - 1] == ' ') + sectionBuilder.Length--; + + sectionBuilder.Append (')'); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + sectionBuilder.Append ((string) token.Value); + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseBracket, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Atom) { + // this might be a region ("<###>") + var expr = (string) token.Value; + + if (expr.Length > 2 && expr[0] == '<' && expr[expr.Length - 1] == '>') { + var region = expr.Substring (1, expr.Length - 2); + + int.TryParse (region, NumberStyles.None, CultureInfo.InvariantCulture, out offset); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + } + + name = sectionBuilder.ToString (); + + switch (token.Type) { + case ImapTokenType.Literal: + length = (int) token.Value; + size += length; + + stream = CreateStream (uid, name, offset, length); + + try { + do { + if (doAsync) + n = await engine.Stream.ReadAsync (buf, 0, buf.Length, ic.CancellationToken).ConfigureAwait (false); + else + n = engine.Stream.Read (buf, 0, buf.Length, ic.CancellationToken); + + if (n > 0) { + stream.Write (buf, 0, n); + nread += n; + + ctx.Report (nread, size); + } else { + break; + } + } while (true); + + stream.Position = 0; + } catch { + stream.Dispose (); + throw; + } + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + var buffer = Encoding.UTF8.GetBytes ((string) token.Value); + length = buffer.Length; + nread += length; + size += length; + + stream = CreateStream (uid, name, offset, length); + + try { + stream.Write (buffer, 0, length); + ctx.Report (nread, size); + stream.Position = 0; + } catch { + stream.Dispose (); + throw; + } + break; + case ImapTokenType.Nil: + stream = CreateStream (uid, name, offset, 0); + length = 0; + break; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + if (uid.HasValue) + stream = CommitStream (stream, uid.Value, name, offset, length); + + // prevent leaks in the (invalid) case where a section may be returned twice + if (ctx.Contains (index, name, out section)) + section.Stream.Dispose (); + + section = new Section (stream, index, uid, name, offset, length); + await ctx.AddAsync (section, doAsync, ic.CancellationToken).ConfigureAwait (false); + break; + case "UID": + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + value = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + uid = new UniqueId (UidValidity, value); + + await ctx.SetUniqueIdAsync (index, uid.Value, doAsync, ic.CancellationToken).ConfigureAwait (false); + + annotations.UniqueId = uid.Value; + modSeq.UniqueId = uid.Value; + labels.UniqueId = uid.Value; + flags.UniqueId = uid.Value; + break; + case "MODSEQ": + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + modseq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + if (modseq > HighestModSeq) + UpdateHighestModSeq (modseq); + + annotations.ModSeq = modseq; + modSeq.ModSeq = modseq; + labels.ModSeq = modseq; + flags.ModSeq = modseq; + modSeqChanged = true; + break; + case "FLAGS": + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message flags. + flags.Flags = await ImapUtils.ParseFlagsListAsync (engine, atom, flags.Keywords, doAsync, ic.CancellationToken).ConfigureAwait (false); + flagsChanged = true; + break; + case "X-GM-LABELS": + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message labels. + labels.Labels = await ImapUtils.ParseLabelsListAsync (engine, doAsync, ic.CancellationToken).ConfigureAwait (false); + labelsChanged = true; + break; + case "ANNOTATION": + // even though we didn't request this piece of information, the IMAP server + // may send it if another client has recently modified the message annotations. + annotations.Annotations = await ImapUtils.ParseAnnotationsAsync (engine, doAsync, ic.CancellationToken).ConfigureAwait (false); + annotationsChanged = true; + break; + default: + // Unexpected or unknown token (such as XAOL.SPAM.REASON or XAOL-MSGID). Simply read 1 more token (the argument) and ignore. + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) + await SkipParenthesizedList (engine, doAsync, ic.CancellationToken).ConfigureAwait (false); + break; + } + } while (true); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "FETCH", token); + + if (flagsChanged) + OnMessageFlagsChanged (flags); + + if (labelsChanged) + OnMessageLabelsChanged (labels); + + if (annotationsChanged) + OnAnnotationsChanged (annotations); + + if (modSeqChanged) + OnModSeqChanged (modSeq); + } + + static string GetBodyPartQuery (string partSpec, bool headersOnly, out string[] tags) + { + string query; + + if (headersOnly) { + tags = new string[1]; + + if (partSpec.Length > 0) { + query = string.Format ("BODY.PEEK[{0}.MIME]", partSpec); + tags[0] = partSpec + ".MIME"; + } else { + query = "BODY.PEEK[HEADER]"; + tags[0] = "HEADER"; + } + } else { + tags = new string[2]; + + if (partSpec.Length > 0) { + tags[0] = partSpec + ".MIME"; + tags[1] = partSpec; + } else { + tags[0] = "HEADER"; + tags[1] = "TEXT"; + } + + query = string.Format ("BODY.PEEK[{0}] BODY.PEEK[{1}]", tags[0], tags[1]); + } + + return query; + } + + async Task GetHeadersAsync (UniqueId uid, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[HEADER])\r\n", uid.Id); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, "HEADER", out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message headers."); + } finally { + ctx.Dispose (); + } + + return await ParseHeadersAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + async Task GetHeadersAsync (UniqueId uid, string partSpecifier, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + string[] tags; + + var command = string.Format ("UID FETCH {0} ({1})\r\n", uid, GetBodyPartQuery (partSpecifier, true, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, tags[0], out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); + } finally { + ctx.Dispose (); + } + + return await ParseHeadersAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (uid, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (uid, true, cancellationToken, progress); + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual HeaderList GetHeaders (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (uid, partSpecifier, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetHeadersAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (uid, partSpecifier, true, cancellationToken, progress); + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeaders (uid, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeadersAsync (uid, part.PartSpecifier, cancellationToken, progress); + } + + async Task GetHeadersAsync (int index, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[HEADER])\r\n", index + 1); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, "HEADER", out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + } finally { + ctx.Dispose (); + } + + return await ParseHeadersAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + async Task GetHeadersAsync (int index, string partSpecifier, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + string[] tags; + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} ({1})\r\n", seqid, GetBodyPartQuery (partSpecifier, true, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, tags[0], out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part headers."); + } finally { + ctx.Dispose (); + } + + return await ParseHeadersAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (index, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified message headers. + /// + /// + /// Gets the specified message headers. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (index, true, cancellationToken, progress); + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual HeaderList GetHeaders (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (index, partSpecifier, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetHeadersAsync (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetHeadersAsync (index, partSpecifier, true, cancellationToken, progress); + } + + /// + /// Get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override HeaderList GetHeaders (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeaders (index, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part headers. + /// + /// + /// Gets the specified body part headers. + /// + /// The body part headers. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested body part headers. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetHeadersAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetHeadersAsync (index, part.PartSpecifier, cancellationToken, progress); + } + + async Task GetMessageAsync (UniqueId uid, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[])\r\n", uid.Id); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, string.Empty, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + } finally { + ctx.Dispose (); + } + + return await ParseMessageAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeMessage GetMessage (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetMessageAsync (uid, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The UID of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMessageAsync (UniqueId uid, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetMessageAsync (uid, true, cancellationToken, progress); + } + + async Task GetMessageAsync (int index, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + CheckState (true, false); + + var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[])\r\n", index + 1); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, string.Empty, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested message."); + } finally { + ctx.Dispose (); + } + + return await ParseMessageAsync (section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetMessageAsync (index, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified message. + /// + /// + /// Gets the specified message. + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetMessageAsync (index, true, cancellationToken, progress); + } + + async Task GetBodyPartAsync (UniqueId uid, string partSpecifier, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + string[] tags; + + var command = string.Format ("UID FETCH {0} ({1})\r\n", uid, GetBodyPartQuery (partSpecifier, false, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + ChainedStream chained = null; + bool dispose = false; + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + chained = new ChainedStream (); + + foreach (var tag in tags) { + if (!ctx.TryGetSection (uid, tag, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + + if (!(section.Stream is MemoryStream || section.Stream is MemoryBlockStream)) + dispose = true; + + chained.Add (section.Stream); + } + } catch { + if (chained != null) + chained.Dispose (); + + throw; + } finally { + ctx.Dispose (); + } + + var entity = await ParseEntityAsync (chained, dispose, doAsync, cancellationToken).ConfigureAwait (false); + + if (partSpecifier.Length == 0) { + for (int i = entity.Headers.Count; i > 0; i--) { + var header = entity.Headers[i - 1]; + + if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + entity.Headers.RemoveAt (i - 1); + } + } + + return entity; + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual MimeEntity GetBodyPart (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetBodyPartAsync (uid, partSpecifier, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The UID of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetBodyPartAsync (UniqueId uid, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetBodyPartAsync (uid, partSpecifier, true, cancellationToken, progress); + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeEntity GetBodyPart (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPart (uid, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// + /// + /// + /// The body part. + /// The UID of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message body. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetBodyPartAsync (UniqueId uid, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPartAsync (uid, part.PartSpecifier, cancellationToken, progress); + } + + async Task GetBodyPartAsync (int index, string partSpecifier, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (partSpecifier == null) + throw new ArgumentNullException (nameof (partSpecifier)); + + CheckState (true, false); + + string[] tags; + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} ({1})\r\n", seqid, GetBodyPartQuery (partSpecifier, false, out tags)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + ChainedStream chained = null; + bool dispose = false; + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + chained = new ChainedStream (); + + foreach (var tag in tags) { + if (!ctx.TryGetSection (index, tag, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested body part."); + + if (!(section.Stream is MemoryStream || section.Stream is MemoryBlockStream)) + dispose = true; + + chained.Add (section.Stream); + } + } catch { + if (chained != null) + chained.Dispose (); + + throw; + } finally { + ctx.Dispose (); + } + + var entity = await ParseEntityAsync (chained, dispose, doAsync, cancellationToken).ConfigureAwait (false); + + if (partSpecifier.Length == 0) { + for (int i = entity.Headers.Count; i > 0; i--) { + var header = entity.Headers[i - 1]; + + if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + entity.Headers.RemoveAt (i - 1); + } + } + + return entity; + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual MimeEntity GetBodyPart (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetBodyPartAsync (index, partSpecifier, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part specifier. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetBodyPartAsync (int index, string partSpecifier, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetBodyPartAsync (index, partSpecifier, true, cancellationToken, progress); + } + + /// + /// Get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override MimeEntity GetBodyPart (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPart (index, part.PartSpecifier, cancellationToken, progress); + } + + /// + /// Asynchronously get the specified body part. + /// + /// + /// Gets the specified body part. + /// + /// The body part. + /// The index of the message. + /// The body part. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetBodyPartAsync (int index, BodyPart part, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return GetBodyPartAsync (index, part.PartSpecifier, cancellationToken, progress); + } + + async Task GetStreamAsync (UniqueId uid, int offset, int count, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) + return new MemoryStream (); + + var ic = new ImapCommand (Engine, cancellationToken, this, "UID FETCH %u (BODY.PEEK[]<%d.%d>)\r\n", uid.Id, offset, count); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, string.Empty, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return section.Stream; + } + + async Task GetStreamAsync (int index, int offset, int count, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) + return new MemoryStream (); + + var ic = new ImapCommand (Engine, cancellationToken, this, "FETCH %d (BODY.PEEK[]<%d.%d>)\r\n", index + 1, offset, count); + var ctx = new FetchStreamContext (progress); + Section section; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, string.Empty, out section, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return section.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, offset, count, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The UID of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (UniqueId uid, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, offset, count, true, cancellationToken, progress); + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, offset, count, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Fetches a substream of the message. If the starting offset is beyond + /// the end of the message, an empty stream is returned. If the number of + /// bytes desired extends beyond the end of the message, a truncated stream + /// will be returned. + /// + /// The stream. + /// The index of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (int index, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, offset, count, true, cancellationToken, progress); + } + + async Task GetStreamAsync (UniqueId uid, string section, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + CheckState (true, false); + + var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}])\r\n", uid, section); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section s; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, section, out s, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return s.Stream; + } + + /// + /// Get a substream of the specified body part. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, section, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified body part. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (UniqueId uid, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, section, true, cancellationToken, progress); + } + + async Task GetStreamAsync (UniqueId uid, string section, int offset, int count, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (!uid.IsValid) + throw new ArgumentException ("The uid is invalid.", nameof (uid)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) + return new MemoryStream (); + + var range = string.Format (CultureInfo.InvariantCulture, "{0}.{1}", offset, count); + var command = string.Format ("UID FETCH {0} (BODY.PEEK[{1}]<{2}>)\r\n", uid, section, range); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section s; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (uid, section, out s, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return s.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, section, offset, count, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The UID of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is invalid. + /// + /// + /// is null. + /// + /// + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (UniqueId uid, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (uid, section, offset, count, true, cancellationToken, progress); + } + + async Task GetStreamAsync (int index, string section, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + CheckState (true, false); + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var command = string.Format ("FETCH {0} (BODY.PEEK[{1}])\r\n", seqid, section); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section s; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, section, out s, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return s.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, section, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (int index, string section, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, section, true, cancellationToken, progress); + } + + async Task GetStreamAsync (int index, string section, int offset, int count, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (section == null) + throw new ArgumentNullException (nameof (section)); + + if (offset < 0) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count)); + + CheckState (true, false); + + if (count == 0) + return new MemoryStream (); + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + var range = string.Format (CultureInfo.InvariantCulture, "{0}.{1}", offset, count); + var command = string.Format ("FETCH {0} (BODY.PEEK[{1}]<{2}>)\r\n", seqid, section, range); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamContext (progress); + Section s; + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + + if (!ctx.TryGetSection (index, section, out s, true)) + throw new MessageNotFoundException ("The IMAP server did not return the requested stream."); + } finally { + ctx.Dispose (); + } + + return s.Stream; + } + + /// + /// Get a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Stream GetStream (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, section, offset, count, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously gets a substream of the specified message. + /// + /// + /// Gets a substream of the specified message. If the starting offset is beyond + /// the end of the specified section of the message, an empty stream is returned. If + /// the number of bytes desired extends beyond the end of the section, a truncated + /// stream will be returned. + /// For more information about how to construct the , + /// see Section 6.4.5 of RFC3501. + /// + /// The stream. + /// The index of the message. + /// The desired section of the message. + /// The starting offset of the first desired byte. + /// The number of bytes desired. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// is out of range. + /// -or- + /// is negative. + /// -or- + /// is negative. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The IMAP server did not return the requested message stream. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task GetStreamAsync (int index, string section, int offset, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamAsync (index, section, offset, count, true, cancellationToken, progress); + } + + class FetchStreamCallbackContext : FetchStreamContextBase + { + readonly ImapFolder folder; + readonly object callback; + + public FetchStreamCallbackContext (ImapFolder folder, object callback, ITransferProgress progress) : base (progress) + { + this.folder = folder; + this.callback = callback; + } + + Task InvokeCallbackAsync (ImapFolder folder, int index, UniqueId uid, Stream stream, bool doAsync, CancellationToken cancellationToken) + { + if (doAsync) + return ((ImapFetchStreamAsyncCallback) callback) (folder, index, uid, stream, cancellationToken); + + ((ImapFetchStreamCallback) callback) (folder, index, uid, stream); + return Complete; + } + + public override async Task AddAsync (Section section, bool doAsync, CancellationToken cancellationToken) + { + if (section.UniqueId.HasValue) { + await InvokeCallbackAsync (folder, section.Index, section.UniqueId.Value, section.Stream, doAsync, cancellationToken).ConfigureAwait (false); + section.Stream.Dispose (); + } else { + Sections.Add (section); + } + } + + public override async Task SetUniqueIdAsync (int index, UniqueId uid, bool doAsync, CancellationToken cancellationToken) + { + for (int i = 0; i < Sections.Count; i++) { + if (Sections[i].Index == index) { + await InvokeCallbackAsync (folder, index, uid, Sections[i].Stream, doAsync, cancellationToken).ConfigureAwait (false); + Sections[i].Stream.Dispose (); + Sections.RemoveAt (i); + break; + } + } + } + } + + async Task GetStreamsAsync (IList uids, object callback, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + + if (uids.Count == 0) + return; + + var ctx = new FetchStreamCallbackContext (this, callback, progress); + var command = "UID FETCH %s (BODY.PEEK[])\r\n"; + + try { + foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } + } finally { + ctx.Dispose (); + } + } + + async Task GetStreamsAsync (IList indexes, object callback, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + CheckAllowIndexes (); + + if (indexes.Count == 0) + return; + + var set = ImapUtils.FormatIndexSet (indexes); + var command = string.Format ("FETCH {0} (UID BODY.PEEK[])\r\n", set); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamCallbackContext (this, callback, progress); + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } finally { + ctx.Dispose (); + } + } + + async Task GetStreamsAsync (int min, int max, object callback, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + if (min < 0) + throw new ArgumentOutOfRangeException (nameof (min)); + + if (max != -1 && max < min) + throw new ArgumentOutOfRangeException (nameof (max)); + + if (callback == null) + throw new ArgumentNullException (nameof (callback)); + + CheckState (true, false); + CheckAllowIndexes (); + + if (min == Count) + return; + + var command = string.Format ("FETCH {0} (UID BODY.PEEK[])\r\n", GetFetchRange (min, max)); + var ic = new ImapCommand (Engine, cancellationToken, this, command); + var ctx = new FetchStreamCallbackContext (this, callback, progress); + + ic.RegisterUntaggedHandler ("FETCH", FetchStreamAsync); + ic.UserData = ctx; + + Engine.QueueCommand (ic); + + try { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("FETCH", ic); + } finally { + ctx.Dispose (); + } + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The uids of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (IList uids, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + GetStreamsAsync (uids, callback, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The uids of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetStreamsAsync (IList uids, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamsAsync (uids, callback, true, cancellationToken, progress); + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The indexes of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (IList indexes, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + GetStreamsAsync (indexes, callback, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The indexes of the messages. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetStreamsAsync (IList indexes, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamsAsync (indexes, callback, true, cancellationToken, progress); + } + + /// + /// Get the streams for the specified messages. + /// + /// + /// Gets the streams for the specified messages. + /// + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual void GetStreams (int min, int max, ImapFetchStreamCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + GetStreamsAsync (min, max, callback, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the streams for the specified messages. + /// + /// + /// Asynchronously gets the streams for the specified messages. + /// + /// An awaitable task. + /// The minimum index. + /// The maximum index, or -1 to specify no upper bound. + /// + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is out of range. + /// -or- + /// is out of range. + /// + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task GetStreamsAsync (int min, int max, ImapFetchStreamAsyncCallback callback, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + return GetStreamsAsync (min, max, callback, true, cancellationToken, progress); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapFolderFlags.cs b/src/MailKit/Net/Imap/ImapFolderFlags.cs new file mode 100644 index 0000000..1884e53 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolderFlags.cs @@ -0,0 +1,2888 @@ +// +// ImapFolderFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + void ProcessUnmodified (ImapCommand ic, ref UniqueIdSet uids, ulong? modseq) + { + if (modseq.HasValue) { + foreach (var rc in ic.RespCodes.OfType ()) { + if (uids != null) + uids.AddRange (rc.UidSet); + else + uids = rc.UidSet; + } + } + } + + IList GetUnmodified (ImapCommand ic, ulong? modseq) + { + if (modseq.HasValue) { + var rc = ic.RespCodes.OfType ().FirstOrDefault (); + + if (rc != null) { + var unmodified = new int[rc.UidSet.Count]; + for (int i = 0; i < unmodified.Length; i++) + unmodified[i] = (int) (rc.UidSet[i].Id - 1); + + return unmodified; + } + } + + return new int[0]; + } + + async Task> ModifyFlagsAsync (IList uids, ulong? modseq, MessageFlags flags, HashSet keywords, string action, bool doAsync, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, true); + + if (uids.Count == 0) + return new UniqueId[0]; + + var flaglist = ImapUtils.FormatFlagsList (flags & PermanentFlags, keywords != null ? keywords.Count : 0); + var keywordList = keywords != null ? keywords.ToArray () : new object[0]; + UniqueIdSet unmodified = null; + var @params = string.Empty; + + if (modseq.HasValue) + @params = string.Format (CultureInfo.InvariantCulture, " (UNCHANGEDSINCE {0})", modseq.Value); + + var command = string.Format ("UID STORE %s{0} {1} {2}\r\n", @params, action, flaglist); + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, command, uids, keywordList)) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + ProcessUnmodified (ic, ref unmodified, modseq); + } + + if (unmodified == null) + return new UniqueId[0]; + + return unmodified; + } + + /// + /// Adds a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void AddFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + bool emptyUserFlags = keywords == null || keywords.Count == 0; + + if ((flags & SettableFlags) == 0 && emptyUserFlags) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + ModifyFlagsAsync (uids, null, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AddFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + bool emptyUserFlags = keywords == null || keywords.Count == 0; + + if ((flags & SettableFlags) == 0 && emptyUserFlags) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, null, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", true, cancellationToken); + } + + /// + /// Removes a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void RemoveFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + ModifyFlagsAsync (uids, null, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task RemoveFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, null, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", true, cancellationToken); + } + + /// + /// Sets the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetFlags (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + ModifyFlagsAsync (uids, null, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetFlagsAsync (IList uids, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (uids, null, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", true, cancellationToken); + } + + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList AddFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AddFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", true, cancellationToken); + } + + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList RemoveFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> RemoveFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", true, cancellationToken); + } + + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList SetFlags (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SetFlagsAsync (IList uids, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (uids, modseq, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", true, cancellationToken); + } + + async Task> ModifyFlagsAsync (IList indexes, ulong? modseq, MessageFlags flags, HashSet keywords, string action, bool doAsync, CancellationToken cancellationToken) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (modseq.HasValue && !supportsModSeq) + throw new NotSupportedException ("The ImapFolder does not support mod-sequences."); + + CheckState (true, true); + + if (indexes.Count == 0) + return new int[0]; + + var flaglist = ImapUtils.FormatFlagsList (flags & PermanentFlags, keywords != null ? keywords.Count : 0); + var keywordList = keywords != null ? keywords.ToArray () : new object [0]; + var set = ImapUtils.FormatIndexSet (indexes); + var @params = string.Empty; + + if (modseq.HasValue) + @params = string.Format (CultureInfo.InvariantCulture, " (UNCHANGEDSINCE {0})", modseq.Value); + + var format = string.Format ("STORE {0}{1} {2} {3}\r\n", set, @params, action, flaglist); + var ic = Engine.QueueCommand (cancellationToken, this, format, keywordList); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + return GetUnmodified (ic, modseq); + } + + /// + /// Adds a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void AddFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously adds a set of flags to the specified messages. + /// + /// + /// Adds a set of flags to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AddFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", true, cancellationToken); + } + + /// + /// Removes a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void RemoveFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously removes a set of flags from the specified messages. + /// + /// + /// Removes a set of flags from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task RemoveFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", true, cancellationToken); + } + + /// + /// Sets the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetFlags (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sets the flags of the specified messages. + /// + /// + /// Sets the flags of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetFlagsAsync (IList indexes, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (indexes, null, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", true, cancellationToken); + } + + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList AddFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of flags to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to add. + /// A set of user-defined flags to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AddFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "+FLAGS.SILENT" : "+FLAGS", true, cancellationToken); + } + + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList RemoveFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of flags from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to remove. + /// A set of user-defined flags to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No flags were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> RemoveFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if ((flags & SettableFlags) == 0 && (keywords == null || keywords.Count == 0)) + throw new ArgumentException ("No flags were specified.", nameof (flags)); + + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "-FLAGS.SILENT" : "-FLAGS", true, cancellationToken); + } + + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList SetFlags (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the flags of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The message flags to set. + /// A set of user-defined flags to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The does not support mod-sequences. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SetFlagsAsync (IList indexes, ulong modseq, MessageFlags flags, HashSet keywords, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + return ModifyFlagsAsync (indexes, modseq, flags, keywords, silent ? "FLAGS.SILENT" : "FLAGS", true, cancellationToken); + } + + string LabelListToString (IList labels, ICollection args) + { + var list = new StringBuilder ("("); + + for (int i = 0; i < labels.Count; i++) { + if (i > 0) + list.Append (' '); + + if (labels[i] == null) { + list.Append ("NIL"); + continue; + } + + switch (labels[i]) { + case "\\AllMail": + case "\\Drafts": + case "\\Important": + case "\\Inbox": + case "\\Spam": + case "\\Sent": + case "\\Starred": + case "\\Trash": + list.Append (labels[i]); + break; + default: + list.Append ("%S"); + args.Add (Engine.EncodeMailboxName (labels[i])); + break; + } + } + + list.Append (')'); + + return list.ToString (); + } + + async Task> ModifyLabelsAsync (IList uids, ulong? modseq, IList labels, string action, bool doAsync, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + + CheckState (true, true); + + if (uids.Count == 0) + return new UniqueId[0]; + + var @params = string.Empty; + + if (modseq.HasValue) + @params = string.Format (CultureInfo.InvariantCulture, " (UNCHANGEDSINCE {0})", modseq.Value); + + var args = new List (); + var list = LabelListToString (labels, args); + var command = string.Format ("UID STORE %s{0} {1} {2}\r\n", @params, action, list); + UniqueIdSet unmodified = null; + + foreach (var ic in Engine.QueueCommands (cancellationToken, this, command, uids, args.ToArray ())) { + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + ProcessUnmodified (ic, ref unmodified, modseq); + } + + if (unmodified == null) + return new UniqueId[0]; + + return unmodified; + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void AddLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + ModifyLabelsAsync (uids, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AddLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", true, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void RemoveLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + ModifyLabelsAsync (uids, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task RemoveLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", true, cancellationToken); + } + + /// + /// Set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetLabels (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + ModifyLabelsAsync (uids, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously set the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The UIDs of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetLabelsAsync (IList uids, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (uids, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", true, cancellationToken); + } + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList AddLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AddLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", true, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList RemoveLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> RemoveLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", true, cancellationToken); + } + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList SetLabels (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The unique IDs of the messages that were not updated. + /// The UIDs of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SetLabelsAsync (IList uids, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (uids, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", true, cancellationToken); + } + + async Task> ModifyLabelsAsync (IList indexes, ulong? modseq, IList labels, string action, bool doAsync, CancellationToken cancellationToken) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The IMAP server does not support the Google Mail extensions."); + + CheckState (true, true); + + if (indexes.Count == 0) + return new int[0]; + + var set = ImapUtils.FormatIndexSet (indexes); + var @params = string.Empty; + + if (modseq.HasValue) + @params = string.Format (CultureInfo.InvariantCulture, " (UNCHANGEDSINCE {0})", modseq.Value); + + var args = new List (); + var list = LabelListToString (labels, args); + var format = string.Format ("STORE {0}{1} {2} {3}\r\n", set, @params, action, list); + var ic = Engine.QueueCommand (cancellationToken, this, format, args.ToArray ()); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("STORE", ic); + + return GetUnmodified (ic, modseq); + } + + /// + /// Add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void AddLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + ModifyLabelsAsync (indexes, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously add a set of labels to the specified messages. + /// + /// + /// Adds a set of labels to the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task AddLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, null, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", true, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void RemoveLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + ModifyLabelsAsync (indexes, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove a set of labels from the specified messages. + /// + /// + /// Removes a set of labels from the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task RemoveLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, null, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", true, cancellationToken); + } + + /// + /// Sets the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override void SetLabels (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + ModifyLabelsAsync (indexes, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sets the labels of the specified messages. + /// + /// + /// Sets the labels of the specified messages. + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SetLabelsAsync (IList indexes, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (indexes, null, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", true, cancellationToken); + } + + /// + /// Add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList AddLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously add a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Adds a set of labels to the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to add. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> AddLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "+X-GM-LABELS.SILENT" : "+X-GM-LABELS", true, cancellationToken); + } + + /// + /// Remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList RemoveLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously remove a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Removes a set of labels from the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to remove. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// -or- + /// No labels were specified. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> RemoveLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + if (labels.Count == 0) + throw new ArgumentException ("No labels were specified.", nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "-X-GM-LABELS.SILENT" : "-X-GM-LABELS", true, cancellationToken); + } + + /// + /// Set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList SetLabels (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously set the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// + /// Sets the labels of the specified messages only if their mod-sequence value is less than the specified value. + /// + /// The indexes of the messages that were not updated. + /// The indexes of the messages. + /// The mod-sequence value. + /// The labels to set. + /// If set to true, no events will be emitted. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the is invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open in read-write mode. + /// + /// + /// The IMAP server does not support the Google Mail Extensions. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SetLabelsAsync (IList indexes, ulong modseq, IList labels, bool silent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (labels == null) + throw new ArgumentNullException (nameof (labels)); + + return ModifyLabelsAsync (indexes, modseq, labels, silent ? "X-GM-LABELS.SILENT" : "X-GM-LABELS", true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapFolderSearch.cs b/src/MailKit/Net/Imap/ImapFolderSearch.cs new file mode 100644 index 0000000..fe26cce --- /dev/null +++ b/src/MailKit/Net/Imap/ImapFolderSearch.cs @@ -0,0 +1,1773 @@ +// +// ImapFolderSearch.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MailKit.Search; + +namespace MailKit.Net.Imap +{ + public partial class ImapFolder + { + static bool IsAscii (string text) + { + for (int i = 0; i < text.Length; i++) { + if (text[i] > 127) + return false; + } + + return true; + } + + static string FormatDateTime (DateTime date) + { + return date.ToString ("d-MMM-yyyy", CultureInfo.InvariantCulture); + } + + bool IsBadCharset (ImapCommand ic, string charset) + { + // Note: if `charset` is null, then the charset is actually US-ASCII... + return ic.Response == ImapCommandResponse.No && + ic.RespCodes.Any (rc => rc.Type == ImapResponseCodeType.BadCharset) && + charset != null && !Engine.SupportedCharsets.Contains (charset); + } + + void AddTextArgument (StringBuilder builder, List args, string text, ref string charset) + { + if (IsAscii (text)) { + builder.Append ("%S"); + args.Add (text); + return; + } + + if (Engine.SupportedCharsets.Contains ("UTF-8")) { + builder.Append ("%S"); + charset = "UTF-8"; + args.Add (text); + return; + } + + // force the text into US-ASCII... + var buffer = new byte[text.Length]; + for (int i = 0; i < text.Length; i++) + buffer[i] = (byte) text[i]; + + builder.Append ("%L"); + args.Add (buffer); + } + + void BuildQuery (StringBuilder builder, SearchQuery query, List args, bool parens, ref string charset) + { + AnnotationSearchQuery annotation; + NumericSearchQuery numeric; + FilterSearchQuery filter; + HeaderSearchQuery header; + BinarySearchQuery binary; + UnarySearchQuery unary; + DateSearchQuery date; + TextSearchQuery text; + UidSearchQuery uid; + + switch (query.Term) { + case SearchTerm.All: + builder.Append ("ALL"); + break; + case SearchTerm.And: + binary = (BinarySearchQuery) query; + if (parens) + builder.Append ('('); + BuildQuery (builder, binary.Left, args, false, ref charset); + builder.Append (' '); + BuildQuery (builder, binary.Right, args, false, ref charset); + if (parens) + builder.Append (')'); + break; + case SearchTerm.Annotation: + if ((Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The ANNOTATION search term is not supported by the IMAP server."); + + annotation = (AnnotationSearchQuery) query; + builder.AppendFormat ("ANNOTATION {0} {1} %S", annotation.Entry, annotation.Attribute); + args.Add (annotation.Value); + break; + case SearchTerm.Answered: + builder.Append ("ANSWERED"); + break; + case SearchTerm.BccContains: + text = (TextSearchQuery) query; + builder.Append ("BCC "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.BodyContains: + text = (TextSearchQuery) query; + builder.Append ("BODY "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.CcContains: + text = (TextSearchQuery) query; + builder.Append ("CC "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Deleted: + builder.Append ("DELETED"); + break; + case SearchTerm.DeliveredAfter: + date = (DateSearchQuery) query; + builder.AppendFormat ("SINCE {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.DeliveredBefore: + date = (DateSearchQuery) query; + builder.AppendFormat ("BEFORE {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.DeliveredOn: + date = (DateSearchQuery) query; + builder.AppendFormat ("ON {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.Draft: + builder.Append ("DRAFT"); + break; + case SearchTerm.Filter: + if ((Engine.Capabilities & ImapCapabilities.Filters) == 0) + throw new NotSupportedException ("The FILTER search term is not supported by the IMAP server."); + + filter = (FilterSearchQuery) query; + builder.Append ("FILTER %S"); + args.Add (filter.Name); + break; + case SearchTerm.Flagged: + builder.Append ("FLAGGED"); + break; + case SearchTerm.FromContains: + text = (TextSearchQuery) query; + builder.Append ("FROM "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Fuzzy: + if ((Engine.Capabilities & ImapCapabilities.FuzzySearch) == 0) + throw new NotSupportedException ("The FUZZY search term is not supported by the IMAP server."); + + builder.Append ("FUZZY "); + unary = (UnarySearchQuery) query; + BuildQuery (builder, unary.Operand, args, true, ref charset); + break; + case SearchTerm.HeaderContains: + header = (HeaderSearchQuery) query; + builder.AppendFormat ("HEADER {0} ", header.Field); + AddTextArgument (builder, args, header.Value, ref charset); + break; + case SearchTerm.Keyword: + text = (TextSearchQuery) query; + builder.Append ("KEYWORD "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.LargerThan: + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "LARGER {0}", numeric.Value); + break; + case SearchTerm.MessageContains: + text = (TextSearchQuery) query; + builder.Append ("TEXT "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.ModSeq: + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "MODSEQ {0}", numeric.Value); + break; + case SearchTerm.New: + builder.Append ("NEW"); + break; + case SearchTerm.Not: + builder.Append ("NOT "); + unary = (UnarySearchQuery) query; + BuildQuery (builder, unary.Operand, args, true, ref charset); + break; + case SearchTerm.NotAnswered: + builder.Append ("UNANSWERED"); + break; + case SearchTerm.NotDeleted: + builder.Append ("UNDELETED"); + break; + case SearchTerm.NotDraft: + builder.Append ("UNDRAFT"); + break; + case SearchTerm.NotFlagged: + builder.Append ("UNFLAGGED"); + break; + case SearchTerm.NotKeyword: + text = (TextSearchQuery) query; + builder.Append ("UNKEYWORD "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.NotRecent: + builder.Append ("OLD"); + break; + case SearchTerm.NotSeen: + builder.Append ("UNSEEN"); + break; + case SearchTerm.Older: + if ((Engine.Capabilities & ImapCapabilities.Within) == 0) + throw new NotSupportedException ("The OLDER search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "OLDER {0}", numeric.Value); + break; + case SearchTerm.Or: + builder.Append ("OR "); + binary = (BinarySearchQuery) query; + BuildQuery (builder, binary.Left, args, true, ref charset); + builder.Append (' '); + BuildQuery (builder, binary.Right, args, true, ref charset); + break; + case SearchTerm.Recent: + builder.Append ("RECENT"); + break; + case SearchTerm.Seen: + builder.Append ("SEEN"); + break; + case SearchTerm.SentBefore: + date = (DateSearchQuery) query; + builder.AppendFormat ("SENTBEFORE {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.SentOn: + date = (DateSearchQuery) query; + builder.AppendFormat ("SENTON {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.SentSince: + date = (DateSearchQuery) query; + builder.AppendFormat ("SENTSINCE {0}", FormatDateTime (date.Date)); + break; + case SearchTerm.SmallerThan: + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "SMALLER {0}", numeric.Value); + break; + case SearchTerm.SubjectContains: + text = (TextSearchQuery) query; + builder.Append ("SUBJECT "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.ToContains: + text = (TextSearchQuery) query; + builder.Append ("TO "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.Uid: + uid = (UidSearchQuery) query; + builder.AppendFormat ("UID {0}", UniqueIdSet.ToString (uid.Uids)); + break; + case SearchTerm.Younger: + if ((Engine.Capabilities & ImapCapabilities.Within) == 0) + throw new NotSupportedException ("The YOUNGER search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "YOUNGER {0}", numeric.Value); + break; + case SearchTerm.GMailMessageId: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-MSGID search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "X-GM-MSGID {0}", numeric.Value); + break; + case SearchTerm.GMailThreadId: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-THRID search term is not supported by the IMAP server."); + + numeric = (NumericSearchQuery) query; + builder.AppendFormat (CultureInfo.InvariantCulture, "X-GM-THRID {0}", numeric.Value); + break; + case SearchTerm.GMailLabels: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-LABELS search term is not supported by the IMAP server."); + + text = (TextSearchQuery) query; + builder.Append ("X-GM-LABELS "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + case SearchTerm.GMailRaw: + if ((Engine.Capabilities & ImapCapabilities.GMailExt1) == 0) + throw new NotSupportedException ("The X-GM-RAW search term is not supported by the IMAP server."); + + text = (TextSearchQuery) query; + builder.Append ("X-GM-RAW "); + AddTextArgument (builder, args, text.Text, ref charset); + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + + string BuildQueryExpression (SearchQuery query, List args, out string charset) + { + var builder = new StringBuilder (); + + charset = null; + + BuildQuery (builder, query, args, false, ref charset); + + return builder.ToString (); + } + + string BuildSortOrder (IList orderBy) + { + var builder = new StringBuilder (); + + builder.Append ('('); + for (int i = 0; i < orderBy.Count; i++) { + if (builder.Length > 1) + builder.Append (' '); + + if (orderBy[i].Order == SortOrder.Descending) + builder.Append ("REVERSE "); + + switch (orderBy[i].Type) { + case OrderByType.Annotation: + if ((Engine.Capabilities & ImapCapabilities.Annotate) == 0) + throw new NotSupportedException ("The ANNOTATION search term is not supported by the IMAP server."); + + var annotation = (OrderByAnnotation) orderBy[i]; + builder.AppendFormat ("ANNOTATION {0} {1}", annotation.Entry, annotation.Attribute); + break; + case OrderByType.Arrival: builder.Append ("ARRIVAL"); break; + case OrderByType.Cc: builder.Append ("CC"); break; + case OrderByType.Date: builder.Append ("DATE"); break; + case OrderByType.DisplayFrom: + if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); + + builder.Append ("DISPLAYFROM"); + break; + case OrderByType.DisplayTo: + if ((Engine.Capabilities & ImapCapabilities.SortDisplay) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT=DISPLAY extension."); + + builder.Append ("DISPLAYTO"); + break; + case OrderByType.From: builder.Append ("FROM"); break; + case OrderByType.Size: builder.Append ("SIZE"); break; + case OrderByType.Subject: builder.Append ("SUBJECT"); break; + case OrderByType.To: builder.Append ("TO"); break; + default: throw new ArgumentOutOfRangeException (); + } + } + builder.Append (')'); + + return builder.ToString (); + } + + static async Task SearchMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var results = (SearchResults) ic.UserData; + var uids = results.UniqueIds; + ImapToken token; + uint uid; + + do { + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" + if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) + break; + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + uids.Add (new UniqueId (ic.Folder.UidValidity, uid)); + } while (true); + + if (token.Type == ImapTokenType.OpenParen) { + await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + + var atom = (string) token.Value; + + switch (atom.ToUpperInvariant ()) { + case "MODSEQ": + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + break; + } + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + results.UniqueIds = uids; + } + + static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + var results = (SearchResults) ic.UserData; + int parenDepth = 0; + //bool uid = false; + string atom; + string tag; + + if (token.Type == ImapTokenType.OpenParen) { + // optional search correlator + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + if (atom == "TAG") { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + tag = (string) token.Value; + + if (tag != ic.Tag) + throw new ImapProtocolException ("Unexpected TAG value in untagged ESEARCH response: " + tag); + } + } while (true); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + //uid = true; + } + + do { + if (token.Type == ImapTokenType.CloseParen) { + if (parenDepth == 0) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + parenDepth--; + } + + if (token.Type == ImapTokenType.Eoln) { + // unget the eoln token + engine.Stream.UngetToken (token); + break; + } + + if (token.Type == ImapTokenType.OpenParen) { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + parenDepth++; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + switch (atom.ToUpperInvariant ()) { + case "RELEVANCY": + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy = new List (); + + do { + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var score = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + if (score > 100) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy.Add ((byte) score); + } while (true); + break; + case "MODSEQ": + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + break; + case "COUNT": + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var count = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Count = (int) count; + break; + case "MIN": + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var min = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Min = new UniqueId (ic.Folder.UidValidity, min); + break; + case "MAX": + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var max = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Max = new UniqueId (ic.Folder.UidValidity, max); + break; + case "ALL": + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var uids = ImapEngine.ParseUidSet (token, ic.Folder.UidValidity, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Count = uids.Count; + results.UniqueIds = uids; + break; + default: + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + } + + token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } while (true); + } + + async Task SearchAsync (string query, bool doAsync, CancellationToken cancellationToken) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + query = query.Trim (); + + if (query.Length == 0) + throw new ArgumentException ("Cannot search using an empty query.", nameof (query)); + + CheckState (true, false); + + var command = "UID SEARCH " + query + "\r\n"; + var ic = new ImapCommand (Engine, cancellationToken, this, command); + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + + // Note: always register the untagged SEARCH handler because some servers will brokenly + // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended + // search syntax. + ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.UserData = new SearchResults (SortOrder.Ascending); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SEARCH", ic); + + return (SearchResults) ic.UserData; + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual SearchResults Search (string query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (query, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Sends a UID SEARCH command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SEARCH command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task SearchAsync (string query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (query, true, cancellationToken); + } + + async Task> SearchAsync (SearchQuery query, bool doAsync, bool retry, CancellationToken cancellationToken) + { + var args = new List (); + string charset; + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID SEARCH "; + + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + command += "RETURN () "; + + if (charset != null && args.Count > 0 && !Engine.UTF8Enabled) + command += "CHARSET " + charset + " "; + + command += expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + + // Note: always register the untagged SEARCH handler because some servers will brokenly + // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended + // search syntax. + ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.UserData = new SearchResults (SortOrder.Ascending); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await SearchAsync (query, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("SEARCH", ic); + } + + return ((SearchResults) ic.UserData).UniqueIds; + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Search (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (query, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// The returned array of unique identifiers can be used with methods such as + /// . + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SearchAsync (SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (query, true, true, cancellationToken); + } + + async Task SearchAsync (SearchOptions options, SearchQuery query, bool doAsync, bool retry, CancellationToken cancellationToken) + { + var args = new List (); + string charset; + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + if ((Engine.Capabilities & ImapCapabilities.ESearch) == 0) + throw new NotSupportedException ("The IMAP server does not support the ESEARCH extension."); + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID SEARCH RETURN ("; + + if (options != SearchOptions.All && options != 0) { + if ((options & SearchOptions.All) != 0) + command += "ALL "; + if ((options & SearchOptions.Relevancy) != 0) + command += "RELEVANCY "; + if ((options & SearchOptions.Count) != 0) + command += "COUNT "; + if ((options & SearchOptions.Min) != 0) + command += "MIN "; + if ((options & SearchOptions.Max) != 0) + command += "MAX "; + command = command.TrimEnd (); + } + command += ") "; + + if (charset != null && args.Count > 0 && !Engine.UTF8Enabled) + command += "CHARSET " + charset + " "; + + command += expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + + // Note: always register the untagged SEARCH handler because some servers will brokenly + // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended + // search syntax. + ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.UserData = new SearchResults (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await SearchAsync (options, query, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("SEARCH", ic); + } + + return (SearchResults) ic.UserData; + } + + /// + /// Search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESEARCH extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override SearchResults Search (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (options, query, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously search the folder for messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, + /// returning only the specified search results. + /// + /// The search results. + /// The search options. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESEARCH extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SearchAsync (SearchOptions options, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SearchAsync (options, query, true, true, cancellationToken); + } + + async Task SortAsync (string query, bool doAsync, CancellationToken cancellationToken) + { + if (query == null) + throw new ArgumentNullException (nameof (query)); + + query = query.Trim (); + + if (query.Length == 0) + throw new ArgumentException ("Cannot sort using an empty query.", nameof (query)); + + if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT extension."); + + CheckState (true, false); + + var command = "UID SORT " + query + "\r\n"; + var ic = new ImapCommand (Engine, cancellationToken, this, command); + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("SORT", SearchMatchesAsync); + ic.UserData = new SearchResults (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) + throw ImapCommandException.Create ("SORT", ic); + + return (SearchResults) ic.UserData; + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual SearchResults Sort (string query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (query, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Sends a UID SORT command with the specified query passed directly to the IMAP server + /// with no interpretation by MailKit. This means that the query may contain any arguments that a + /// UID SORT command is allowed to have according to the IMAP specifications and any + /// extensions that are supported, including RETURN parameters. + /// + /// An array of matching UIDs. + /// The search query. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The IMAP server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public virtual Task SortAsync (string query, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (query, true, cancellationToken); + } + + async Task> SortAsync (SearchQuery query, IList orderBy, bool doAsync, bool retry, CancellationToken cancellationToken) + { + var args = new List (); + string charset; + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + CheckState (true, false); + + if ((Engine.Capabilities & ImapCapabilities.Sort) == 0) + throw new NotSupportedException ("The IMAP server does not support the SORT extension."); + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var order = BuildSortOrder (orderBy); + var command = "UID SORT "; + + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + command += "RETURN () "; + + command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + else + ic.RegisterUntaggedHandler ("SORT", SearchMatchesAsync); + ic.UserData = new SearchResults (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await SortAsync (query, orderBy, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("SORT", ic); + } + + return ((SearchResults) ic.UserData).UniqueIds; + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Sort (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (query, orderBy, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// The returned array of unique identifiers will be sorted in the preferred order and + /// can be used with . + /// + /// An array of matching UIDs in the specified sort order. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the SORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> SortAsync (SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (query, orderBy, true, true, cancellationToken); + } + + async Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, bool doAsync, bool retry, CancellationToken cancellationToken) + { + var args = new List (); + string charset; + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + if (orderBy == null) + throw new ArgumentNullException (nameof (orderBy)); + + if (orderBy.Count == 0) + throw new ArgumentException ("No sort order provided.", nameof (orderBy)); + + CheckState (true, false); + + if ((Engine.Capabilities & ImapCapabilities.ESort) == 0) + throw new NotSupportedException ("The IMAP server does not support the ESORT extension."); + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var order = BuildSortOrder (orderBy); + + var command = "UID SORT RETURN ("; + if (options != SearchOptions.All && options != 0) { + if ((options & SearchOptions.All) != 0) + command += "ALL "; + if ((options & SearchOptions.Relevancy) != 0) + command += "RELEVANCY "; + if ((options & SearchOptions.Count) != 0) + command += "COUNT "; + if ((options & SearchOptions.Min) != 0) + command += "MIN "; + if ((options & SearchOptions.Max) != 0) + command += "MAX "; + command = command.TrimEnd (); + } + command += ") "; + + command += order + " " + (charset ?? "US-ASCII") + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.UserData = new SearchResults (); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await SortAsync (options, query, orderBy, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("SORT", ic); + } + + return (SearchResults) ic.UserData; + } + + /// + /// Sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override SearchResults Sort (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (options, query, orderBy, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously sort messages matching the specified query. + /// + /// + /// Searches the folder for messages matching the specified query, returning the search results in the specified sort order. + /// + /// The search results. + /// The search options. + /// The search query. + /// The sort order. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The IMAP server does not support the ESORT extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task SortAsync (SearchOptions options, SearchQuery query, IList orderBy, CancellationToken cancellationToken = default (CancellationToken)) + { + return SortAsync (options, query, orderBy, true, true, cancellationToken); + } + + static async Task ThreadMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + ic.UserData = await ImapUtils.ParseThreadsAsync (engine, ic.Folder.UidValidity, doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + async Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, bool doAsync, bool retry, CancellationToken cancellationToken) + { + var method = algorithm.ToString ().ToUpperInvariant (); + var args = new List (); + string charset; + + if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) + throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); + + if (!Engine.ThreadingAlgorithms.Contains (algorithm)) + throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; + + command += expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("THREAD", ThreadMatchesAsync); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await ThreadAsync (algorithm, query, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("THREAD", ic); + } + + var threads = (IList) ic.UserData; + + if (threads == null) + return new MessageThread[0]; + + return threads; + } + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Thread (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return ThreadAsync (algorithm, query, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return ThreadAsync (algorithm, query, true, true, cancellationToken); + } + + async Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, bool doAsync, bool retry, CancellationToken cancellationToken) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if ((Engine.Capabilities & ImapCapabilities.Thread) == 0) + throw new NotSupportedException ("The IMAP server does not support the THREAD extension."); + + if (!Engine.ThreadingAlgorithms.Contains (algorithm)) + throw new ArgumentOutOfRangeException (nameof (algorithm), "The specified threading algorithm is not supported."); + + if (query == null) + throw new ArgumentNullException (nameof (query)); + + CheckState (true, false); + + if (uids.Count == 0) + return new MessageThread[0]; + + var method = algorithm.ToString ().ToUpperInvariant (); + var set = UniqueIdSet.ToString (uids); + var args = new List (); + string charset; + + var optimized = query.Optimize (new ImapSearchQueryOptimizer ()); + var expr = BuildQueryExpression (optimized, args, out charset); + var command = "UID THREAD " + method + " " + (charset ?? "US-ASCII") + " "; + + command += "UID " + set + " " + expr + "\r\n"; + + var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); + ic.RegisterUntaggedHandler ("THREAD", ThreadMatchesAsync); + + Engine.QueueCommand (ic); + + await Engine.RunAsync (ic, doAsync).ConfigureAwait (false); + + ProcessResponseCodes (ic, null); + + if (ic.Response != ImapCommandResponse.Ok) { + if (retry && IsBadCharset (ic, charset)) + return await ThreadAsync (uids, algorithm, query, doAsync, false, cancellationToken).ConfigureAwait (false); + + throw ImapCommandException.Create ("THREAD", ic); + } + + var threads = (IList) ic.UserData; + + if (threads == null) + return new MessageThread[0]; + + return threads; + } + + /// + /// Thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override IList Thread (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return ThreadAsync (uids, algorithm, query, false, true, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously thread the messages in the folder that match the search query using the specified threading algorithm. + /// + /// + /// The can be used with methods such as + /// . + /// + /// An array of message threads. + /// The subset of UIDs + /// The threading algorithm to use. + /// The search query. + /// The cancellation token. + /// + /// is not supported. + /// + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// One or more of the is invalid. + /// + /// + /// One or more search terms in the are not supported by the IMAP server. + /// -or- + /// The server does not support the THREAD extension. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The is not currently open. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The server's response contained unexpected tokens. + /// + /// + /// The server replied with a NO or BAD response. + /// + public override Task> ThreadAsync (IList uids, ThreadingAlgorithm algorithm, SearchQuery query, CancellationToken cancellationToken = default (CancellationToken)) + { + return ThreadAsync (uids, algorithm, query, true, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapImplementation.cs b/src/MailKit/Net/Imap/ImapImplementation.cs new file mode 100644 index 0000000..ee3227e --- /dev/null +++ b/src/MailKit/Net/Imap/ImapImplementation.cs @@ -0,0 +1,217 @@ +// +// ImapImplementation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit.Net.Imap { + /// + /// The details of an IMAP client or server implementation. + /// + /// + /// Allows an IMAP client and server to share their implementation details + /// with each other for the purposes of debugging. + /// + /// + /// + /// + public class ImapImplementation + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + public ImapImplementation () + { + Properties = new Dictionary (); + } + + string GetProperty (string property) + { + string value; + + Properties.TryGetValue (property, out value); + + return value; + } + + /// + /// Get the identification properties. + /// + /// + /// Gets the dictionary of raw identification properties. + /// + /// + /// + /// + /// The properties. + public Dictionary Properties { + get; private set; + } + + /// + /// Get or set the name of the program. + /// + /// + /// Gets or sets the name of the program. + /// + /// + /// + /// + /// The program name. + public string Name { + get { return GetProperty ("name"); } + set { Properties["name"] = value; } + } + + /// + /// Get or set the version of the program. + /// + /// + /// Gets or sets the version of the program. + /// + /// + /// + /// + /// The program version. + public string Version { + get { return GetProperty ("version"); } + set { Properties["version"] = value; } + } + + /// + /// Get or set the name of the operating system. + /// + /// + /// Gets or sets the name of the operating system. + /// + /// The name of the operation system. + public string OS { + get { return GetProperty ("os"); } + set { Properties["os"] = value; } + } + + /// + /// Get or set the version of the operating system. + /// + /// + /// Gets or sets the version of the operating system. + /// + /// The version of the operation system. + public string OSVersion { + get { return GetProperty ("os-version"); } + set { Properties["os-version"] = value; } + } + + /// + /// Get or set the name of the vendor. + /// + /// + /// Gets or sets the name of the vendor. + /// + /// The name of the vendor. + public string Vendor { + get { return GetProperty ("vendor"); } + set { Properties["vendor"] = value; } + } + + /// + /// Get or set the support URL. + /// + /// + /// Gets or sets the support URL. + /// + /// The support URL. + public string SupportUrl { + get { return GetProperty ("support-url"); } + set { Properties["support-url"] = value; } + } + + /// + /// Get or set the postal address of the vendor. + /// + /// + /// Gets or sets the postal address of the vendor. + /// + /// The postal address. + public string Address { + get { return GetProperty ("address"); } + set { Properties["address"] = value; } + } + + /// + /// Get or set the release date of the program. + /// + /// + /// Gets or sets the release date of the program. + /// + /// The release date. + public string ReleaseDate { + get { return GetProperty ("date"); } + set { Properties["date"] = value; } + } + + /// + /// Get or set the command used to start the program. + /// + /// + /// Gets or sets the command used to start the program. + /// + /// The command used to start the program. + public string Command { + get { return GetProperty ("command"); } + set { Properties["command"] = value; } + } + + /// + /// Get or set the command-line arguments used to start the program. + /// + /// + /// Gets or sets the command-line arguments used to start the program. + /// + /// The command-line arguments used to start the program. + public string Arguments { + get { return GetProperty ("arguments"); } + set { Properties["arguments"] = value; } + } + + /// + /// Get or set the environment variables available to the program. + /// + /// + /// Get or set the environment variables available to the program. + /// + /// The environment variables. + public string Environment { + get { return GetProperty ("environment"); } + set { Properties["environment"] = value; } + } + } +} diff --git a/src/MailKit/Net/Imap/ImapProtocolException.cs b/src/MailKit/Net/Imap/ImapProtocolException.cs new file mode 100644 index 0000000..a1aad3d --- /dev/null +++ b/src/MailKit/Net/Imap/ImapProtocolException.cs @@ -0,0 +1,109 @@ +// +// ImapException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Imap { + /// + /// An IMAP protocol exception. + /// + /// + /// The exception that is thrown when there is an error communicating with an IMAP server. An + /// is typically fatal and requires the + /// to be reconnected. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ImapProtocolException : ProtocolException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected ImapProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public ImapProtocolException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public ImapProtocolException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public ImapProtocolException () + { + } + + /// + /// Gets or sets whether or not this exception was thrown due to an unexpected token. + /// + /// + /// Gets or sets whether or not this exception was thrown due to an unexpected token. + /// + /// true if an unexpected token was encountered; otherwise, false. + internal bool UnexpectedToken { + get; set; + } + } +} diff --git a/src/MailKit/Net/Imap/ImapResponseCode.cs b/src/MailKit/Net/Imap/ImapResponseCode.cs new file mode 100644 index 0000000..4d0b2d5 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapResponseCode.cs @@ -0,0 +1,373 @@ +// +// ImapResponseCode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Imap { + enum ImapResponseCodeType : byte { + Alert, + BadCharset, + Capability, + NewName, + Parse, + PermanentFlags, + ReadOnly, + ReadWrite, + TryCreate, + UidNext, + UidValidity, + Unseen, + + // RESP-CODES introduced in rfc2221: + Referral, + + // RESP-CODES introduced in rfc3516, + UnknownCte, + + // RESP-CODES introduced in rfc4315: + AppendUid, + CopyUid, + UidNotSticky, + + // RESP-CODES introduced in rfc4467: + UrlMech, + + // RESP-CODES introduced in rfc4469: + BadUrl, + TooBig, + + // RESP-CODES introduced in rfc4551: + HighestModSeq, + Modified, + NoModSeq, + + // RESP-CODES introduced in rfc4978: + CompressionActive, + + // RESP-CODES introduced in rfc5162: + Closed, + + // RESP-CODES introduced in rfc5182: + NotSaved, + + // RESP-CODES introduced in rfc5255: + BadComparator, + + // RESP-CODES introduced in rfc5257: + Annotate, + Annotations, + + // RESP-CODES introduced in rfc5259: + MaxConvertMessages, + MaxConvertParts, + TempFail, + + // RESP-CODES introduced in rfc5267: + NoUpdate, + + // RESP-CODES introduced in rfc5464: + Metadata, + + // RESP-CODES introduced in rfc5465: + NotificationOverflow, + BadEvent, + + // RESP-CODES introduced in rfc5466: + UndefinedFilter, + + // RESP-CODES introduced in rfc5530: + Unavailable, + AuthenticationFailed, + AuthorizationFailed, + Expired, + PrivacyRequired, + ContactAdmin, + NoPerm, + InUse, + ExpungeIssued, + Corruption, + ServerBug, + ClientBug, + CanNot, + Limit, + OverQuota, + AlreadyExists, + NonExistent, + + // RESP-CODES introduced in rfc6154: + UseAttr, + + // RESP-CODES introduced in rfc8474: + MailboxId, + + Unknown = 255 + } + + class ImapResponseCode + { + public readonly ImapResponseCodeType Type; + public bool IsTagged, IsError; + public string Message; + + internal ImapResponseCode (ImapResponseCodeType type, bool isError) + { + IsError = isError; + Type = type; + } + + public static ImapResponseCode Create (ImapResponseCodeType type) + { + switch (type) { + case ImapResponseCodeType.Alert: return new ImapResponseCode (type, false); + case ImapResponseCodeType.BadCharset: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Capability: return new ImapResponseCode (type, false); + case ImapResponseCodeType.NewName: return new NewNameResponseCode (type); + case ImapResponseCodeType.Parse: return new ImapResponseCode (type, true); + case ImapResponseCodeType.PermanentFlags: return new PermanentFlagsResponseCode (type); + case ImapResponseCodeType.ReadOnly: return new ImapResponseCode (type, false); + case ImapResponseCodeType.ReadWrite: return new ImapResponseCode (type, false); + case ImapResponseCodeType.TryCreate: return new ImapResponseCode (type, true); + case ImapResponseCodeType.UidNext: return new UidNextResponseCode (type); + case ImapResponseCodeType.UidValidity: return new UidValidityResponseCode (type); + case ImapResponseCodeType.Unseen: return new UnseenResponseCode (type); + case ImapResponseCodeType.Referral: return new ImapResponseCode (type, false); + case ImapResponseCodeType.UnknownCte: return new ImapResponseCode (type, true); + case ImapResponseCodeType.AppendUid: return new AppendUidResponseCode (type); + case ImapResponseCodeType.CopyUid: return new CopyUidResponseCode (type); + case ImapResponseCodeType.UidNotSticky: return new ImapResponseCode (type, false); + case ImapResponseCodeType.UrlMech: return new ImapResponseCode (type, false); + case ImapResponseCodeType.BadUrl: return new BadUrlResponseCode (type); + case ImapResponseCodeType.TooBig: return new ImapResponseCode (type, true); + case ImapResponseCodeType.HighestModSeq: return new HighestModSeqResponseCode (type); + case ImapResponseCodeType.Modified: return new ModifiedResponseCode (type); + case ImapResponseCodeType.NoModSeq: return new ImapResponseCode (type, false); + case ImapResponseCodeType.CompressionActive: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Closed: return new ImapResponseCode (type, false); + case ImapResponseCodeType.NotSaved: return new ImapResponseCode (type, true); + case ImapResponseCodeType.BadComparator: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Annotate: return new AnnotateResponseCode (type); + case ImapResponseCodeType.Annotations: return new AnnotationsResponseCode (type); + case ImapResponseCodeType.MaxConvertMessages: return new MaxConvertResponseCode (type); + case ImapResponseCodeType.MaxConvertParts: return new MaxConvertResponseCode (type); + case ImapResponseCodeType.TempFail: return new ImapResponseCode (type, true); + case ImapResponseCodeType.NoUpdate: return new NoUpdateResponseCode (type); + case ImapResponseCodeType.Metadata: return new MetadataResponseCode (type); + case ImapResponseCodeType.NotificationOverflow: return new ImapResponseCode (type, false); + case ImapResponseCodeType.BadEvent: return new ImapResponseCode (type, true); + case ImapResponseCodeType.UndefinedFilter: return new UndefinedFilterResponseCode (type); + case ImapResponseCodeType.Unavailable: return new ImapResponseCode (type, true); + case ImapResponseCodeType.AuthenticationFailed: return new ImapResponseCode (type, true); + case ImapResponseCodeType.AuthorizationFailed: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Expired: return new ImapResponseCode (type, true); + case ImapResponseCodeType.PrivacyRequired: return new ImapResponseCode (type, true); + case ImapResponseCodeType.ContactAdmin: return new ImapResponseCode (type, true); + case ImapResponseCodeType.NoPerm: return new ImapResponseCode (type, true); + case ImapResponseCodeType.InUse: return new ImapResponseCode (type, true); + case ImapResponseCodeType.ExpungeIssued: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Corruption: return new ImapResponseCode (type, true); + case ImapResponseCodeType.ServerBug: return new ImapResponseCode (type, true); + case ImapResponseCodeType.ClientBug: return new ImapResponseCode (type, true); + case ImapResponseCodeType.CanNot: return new ImapResponseCode (type, true); + case ImapResponseCodeType.Limit: return new ImapResponseCode (type, true); + case ImapResponseCodeType.OverQuota: return new ImapResponseCode (type, true); + case ImapResponseCodeType.AlreadyExists: return new ImapResponseCode (type, true); + case ImapResponseCodeType.NonExistent: return new ImapResponseCode (type, true); + case ImapResponseCodeType.UseAttr: return new ImapResponseCode (type, true); + case ImapResponseCodeType.MailboxId: return new MailboxIdResponseCode (type); + default: return new ImapResponseCode (type, true); + } + } + } + + class NewNameResponseCode : ImapResponseCode + { + public string OldName; + public string NewName; + + internal NewNameResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class PermanentFlagsResponseCode : ImapResponseCode + { + public MessageFlags Flags; + + internal PermanentFlagsResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class UidNextResponseCode : ImapResponseCode + { + public UniqueId Uid; + + internal UidNextResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class UidValidityResponseCode : ImapResponseCode + { + public uint UidValidity; + + internal UidValidityResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class UnseenResponseCode : ImapResponseCode + { + public int Index; + + internal UnseenResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class AppendUidResponseCode : UidValidityResponseCode + { + public UniqueIdSet UidSet; + + internal AppendUidResponseCode (ImapResponseCodeType type) : base (type) + { + } + } + + class CopyUidResponseCode : UidValidityResponseCode + { + public UniqueIdSet SrcUidSet, DestUidSet; + + internal CopyUidResponseCode (ImapResponseCodeType type) : base (type) + { + } + } + + class BadUrlResponseCode : ImapResponseCode + { + public string BadUrl; + + internal BadUrlResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class HighestModSeqResponseCode : ImapResponseCode + { + public ulong HighestModSeq; + + internal HighestModSeqResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class ModifiedResponseCode : ImapResponseCode + { + public UniqueIdSet UidSet; + + internal ModifiedResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + class MaxConvertResponseCode : ImapResponseCode + { + public uint MaxConvert; + + internal MaxConvertResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class NoUpdateResponseCode : ImapResponseCode + { + public string Tag; + + internal NoUpdateResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + enum AnnotateResponseCodeSubType + { + TooBig, + TooMany + } + + class AnnotateResponseCode : ImapResponseCode + { + public AnnotateResponseCodeSubType SubType; + + internal AnnotateResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class AnnotationsResponseCode : ImapResponseCode + { + public AnnotationAccess Access; + public AnnotationScope Scopes; + public uint MaxSize; + + internal AnnotationsResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } + + enum MetadataResponseCodeSubType + { + LongEntries, + MaxSize, + TooMany, + NoPrivate + } + + class MetadataResponseCode : ImapResponseCode + { + public MetadataResponseCodeSubType SubType; + public uint Value; + + internal MetadataResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class UndefinedFilterResponseCode : ImapResponseCode + { + public string Name; + + internal UndefinedFilterResponseCode (ImapResponseCodeType type) : base (type, true) + { + } + } + + class MailboxIdResponseCode : ImapResponseCode + { + public string MailboxId; + + internal MailboxIdResponseCode (ImapResponseCodeType type) : base (type, false) + { + } + } +} diff --git a/src/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs b/src/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs new file mode 100644 index 0000000..4cba40e --- /dev/null +++ b/src/MailKit/Net/Imap/ImapSearchQueryOptimizer.cs @@ -0,0 +1,83 @@ +// +// ImapSearchQueryOptimizer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MailKit.Search; + +namespace MailKit.Net.Imap { + class ImapSearchQueryOptimizer : ISearchQueryOptimizer + { + #region ISearchQueryOptimizer implementation + + public SearchQuery Reduce (SearchQuery expr) + { + if (expr.Term == SearchTerm.And) { + var and = (BinarySearchQuery) expr; + + if (and.Left.Term == SearchTerm.All) + return and.Right.Optimize (this); + + if (and.Right.Term == SearchTerm.All) + return and.Left.Optimize (this); + } else if (expr.Term == SearchTerm.Or) { + var or = (BinarySearchQuery) expr; + + if (or.Left.Term == SearchTerm.All) + return SearchQuery.All; + + if (or.Right.Term == SearchTerm.All) + return SearchQuery.All; + } else if (expr.Term == SearchTerm.Not) { + var unary = (UnarySearchQuery) expr; + + switch (unary.Operand.Term) { + case SearchTerm.Not: return ((UnarySearchQuery) unary.Operand).Operand.Optimize (this); + case SearchTerm.NotAnswered: return SearchQuery.Answered; + case SearchTerm.Answered: return SearchQuery.NotAnswered; + case SearchTerm.NotDeleted: return SearchQuery.Deleted; + case SearchTerm.Deleted: return SearchQuery.NotDeleted; + case SearchTerm.NotDraft: return SearchQuery.Draft; + case SearchTerm.Draft: return SearchQuery.NotDraft; + case SearchTerm.NotFlagged: return SearchQuery.Flagged; + case SearchTerm.Flagged: return SearchQuery.NotFlagged; + case SearchTerm.NotRecent: return SearchQuery.Recent; + case SearchTerm.Recent: return SearchQuery.NotRecent; + case SearchTerm.NotSeen: return SearchQuery.Seen; + case SearchTerm.Seen: return SearchQuery.NotSeen; + } + + if (unary.Operand.Term == SearchTerm.Keyword) + return new TextSearchQuery (SearchTerm.NotKeyword, ((TextSearchQuery) unary.Operand).Text); + + if (unary.Operand.Term == SearchTerm.NotKeyword) + return new TextSearchQuery (SearchTerm.Keyword, ((TextSearchQuery) unary.Operand).Text); + } + + return expr; + } + + #endregion + } +} diff --git a/src/MailKit/Net/Imap/ImapStream.cs b/src/MailKit/Net/Imap/ImapStream.cs new file mode 100644 index 0000000..e2df9c0 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapStream.cs @@ -0,0 +1,1190 @@ +// +// ImapStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +using MimeKit.IO; + +using Buffer = System.Buffer; +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Imap { + /// + /// An enumeration of the possible IMAP streaming modes. + /// + /// + /// Normal operation is done in the mode, + /// but when reading literal string data, the + /// mode should be used. + /// + enum ImapStreamMode { + /// + /// Reads 1 token at a time. + /// + Token, + + /// + /// Reads literal string data. + /// + Literal + } + + class ImapStream : Stream, ICancellableStream + { + public const string AtomSpecials = "(){%*\\\"\n"; + public const string DefaultSpecials = "[]" + AtomSpecials; + const int ReadAheadSize = 128; + const int BlockSize = 4096; + const int PadSize = 4; + + static readonly Encoding Latin1; + static readonly Encoding UTF8; + + // I/O buffering + readonly byte[] input = new byte[ReadAheadSize + BlockSize + PadSize]; + const int inputStart = ReadAheadSize; + int inputIndex = ReadAheadSize; + int inputEnd = ReadAheadSize; + + readonly byte[] output = new byte[BlockSize]; + int outputIndex; + + readonly IProtocolLogger logger; + int literalDataLeft; + ImapToken nextToken; + bool disposed; + + static ImapStream () + { + UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); + + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.GetEncoding (1252); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The underlying network stream. + /// The protocol logger. + public ImapStream (Stream source, IProtocolLogger protocolLogger) + { + logger = protocolLogger; + IsConnected = true; + Stream = source; + } + + /// + /// Get or sets the underlying network stream. + /// + /// + /// Gets or sets the underlying network stream. + /// + /// The underlying network stream. + public Stream Stream { + get; internal set; + } + + /// + /// Get or sets the mode used for reading. + /// + /// + /// Gets or sets the mode used for reading. + /// + /// The mode. + public ImapStreamMode Mode { + get; set; + } + + /// + /// Get the length of the literal. + /// + /// + /// Gets the length of the literal. + /// + /// The length of the literal. + public int LiteralLength { + get { return literalDataLeft; } + internal set { literalDataLeft = value; } + } + + /// + /// Get whether or not the stream is connected. + /// + /// + /// Gets whether or not the stream is connected. + /// + /// true if the stream is connected; otherwise, false. + public bool IsConnected { + get; private set; + } + + /// + /// Get whether the stream supports reading. + /// + /// + /// Gets whether the stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return Stream.CanRead; } + } + + /// + /// Get whether the stream supports writing. + /// + /// + /// Gets whether the stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return Stream.CanWrite; } + } + + /// + /// Get whether the stream supports seeking. + /// + /// + /// Gets whether the stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Get whether the stream supports I/O timeouts. + /// + /// + /// Gets whether the stream supports I/O timeouts. + /// + /// true if the stream supports I/O timeouts; otherwise, false. + public override bool CanTimeout { + get { return Stream.CanTimeout; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// The read timeout. + public override int ReadTimeout { + get { return Stream.ReadTimeout; } + set { Stream.ReadTimeout = value; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// The write timeout. + public override int WriteTimeout { + get { return Stream.WriteTimeout; } + set { Stream.WriteTimeout = value; } + } + + /// + /// Get or set the position within the current stream. + /// + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return Stream.Position; } + set { throw new NotSupportedException (); } + } + + /// + /// Get the length of the stream, in bytes. + /// + /// + /// Gets the length of the stream, in bytes. + /// + /// A long value representing the length of the stream in bytes. + /// The length of the stream. + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Length { + get { return Stream.Length; } + } + + async Task ReadAheadAsync (int atleast, bool doAsync, CancellationToken cancellationToken) + { + int left = inputEnd - inputIndex; + + if (left >= atleast) + return left; + + int start = inputStart; + int end = inputEnd; + int nread; + + if (left > 0) { + int index = inputIndex; + + // attempt to align the end of the remaining input with ReadAheadSize + if (index >= start) { + start -= Math.Min (ReadAheadSize, left); + Buffer.BlockCopy (input, index, input, start, left); + index = start; + start += left; + } else if (index > 0) { + int shift = Math.Min (index, end - start); + Buffer.BlockCopy (input, index, input, index - shift, left); + index -= shift; + start = index + left; + } else { + // we can't shift... + start = end; + } + + inputIndex = index; + inputEnd = start; + } else { + inputIndex = start; + inputEnd = start; + } + + end = input.Length - PadSize; + + try { + var network = Stream as NetworkStream; + + cancellationToken.ThrowIfCancellationRequested (); + + if (doAsync) { + nread = await Stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectRead, cancellationToken); + nread = Stream.Read (input, start, end - start); + } + + if (nread > 0) { + logger.LogServer (input, start, nread); + inputEnd += nread; + } else { + throw new ImapProtocolException ("The IMAP server has unexpectedly disconnected."); + } + + if (network == null) + cancellationToken.ThrowIfCancellationRequested (); + } catch { + IsConnected = false; + throw; + } + + return inputEnd - inputIndex; + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (ImapStream)); + } + + async Task ReadAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (Mode != ImapStreamMode.Literal) + return 0; + + count = Math.Min (count, literalDataLeft); + + int length = inputEnd - inputIndex; + int n; + + if (length < count && length <= ReadAheadSize) + await ReadAheadAsync (BlockSize, doAsync, cancellationToken).ConfigureAwait (false); + + length = inputEnd - inputIndex; + n = Math.Min (count, length); + + Buffer.BlockCopy (input, inputIndex, buffer, offset, n); + literalDataLeft -= n; + inputIndex += n; + + if (literalDataLeft == 0) + Mode = ImapStreamMode.Token; + + return n; + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream is in token mode (see ). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream is in token mode (see ). + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + return Read (buffer, offset, count, CancellationToken.None); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync (buffer, offset, count, true, cancellationToken); + } + + static bool IsAtom (byte c, string specials) + { + return !IsCtrl (c) && !IsWhiteSpace (c) && specials.IndexOf ((char) c) == -1; + } + + static bool IsCtrl (byte c) + { + return c <= 0x1f || c == 0x7f; + } + + static bool IsWhiteSpace (byte c) + { + return c == (byte) ' ' || c == (byte) '\t' || c == (byte) '\r'; + } + + async Task ReadQuotedStringTokenAsync (bool doAsync, CancellationToken cancellationToken) + { + bool escaped = false; + + // skip over the opening '"' + inputIndex++; + + using (var memory = new MemoryStream ()) { + do { + while (inputIndex < inputEnd) { + if (input[inputIndex] == (byte) '"' && !escaped) + break; + + if (input[inputIndex] == (byte) '\\' && !escaped) { + escaped = true; + } else { + memory.WriteByte (input[inputIndex]); + escaped = false; + } + + inputIndex++; + } + + if (inputIndex + 1 < inputEnd) { + // skip over closing '"' + inputIndex++; + + // Note: Some IMAP servers do not properly escape double-quotes inside + // of a qstring token and so, as an attempt at working around this + // problem, check that the closing '"' character is not immediately + // followed by any character that we would expect immediately following + // a qstring token. + // + // See https://github.com/jstedfast/MailKit/issues/485 for details. + if ("]) \r\n".IndexOf ((char) input[inputIndex]) != -1) + break; + + memory.WriteByte ((byte) '"'); + continue; + } + + await ReadAheadAsync (2, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var buffer = memory.GetBuffer (); +#else + var buffer = memory.ToArray (); +#endif + int length = (int) memory.Length; + + return new ImapToken (ImapTokenType.QString, Encoding.UTF8.GetString (buffer, 0, length)); + } + } + + async Task ReadAtomStringAsync (bool flag, string specials, bool doAsync, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + do { + input[inputEnd] = (byte) '\n'; + + if (flag && memory.Length == 0 && input[inputIndex] == (byte) '*') { + // this is a special wildcard flag + inputIndex++; + return "*"; + } + + while (IsAtom (input[inputIndex], specials)) + memory.WriteByte (input[inputIndex++]); + + if (inputIndex < inputEnd) + break; + + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + + var count = (int) memory.Length; +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var buf = memory.GetBuffer (); +#else + var buf = memory.ToArray (); +#endif + + try { + return UTF8.GetString (buf, 0, count); + } catch (DecoderFallbackException) { + return Latin1.GetString (buf, 0, count); + } + } + } + + async Task ReadAtomTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) + { + var atom = await ReadAtomStringAsync (false, specials, doAsync, cancellationToken).ConfigureAwait (false); + + return atom == "NIL" ? new ImapToken (ImapTokenType.Nil, atom) : new ImapToken (ImapTokenType.Atom, atom); + } + + async Task ReadFlagTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) + { + inputIndex++; + + var flag = "\\" + await ReadAtomStringAsync (true, specials, doAsync, cancellationToken).ConfigureAwait (false); + + return new ImapToken (ImapTokenType.Flag, flag); + } + + async Task ReadLiteralTokenAsync (bool doAsync, CancellationToken cancellationToken) + { + var builder = new StringBuilder (); + + // skip over the '{' + inputIndex++; + + do { + input[inputEnd] = (byte) '}'; + + while (input[inputIndex] != (byte) '}' && input[inputIndex] != '+') + builder.Append ((char) input[inputIndex++]); + + if (inputIndex < inputEnd) + break; + + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + + if (input[inputIndex] == (byte) '+') + inputIndex++; + + // technically, we need "}\r\n", but in order to be more lenient, we'll accept "}\n" + await ReadAheadAsync (2, doAsync, cancellationToken).ConfigureAwait (false); + + if (input[inputIndex] != (byte) '}') { + // PROTOCOL ERROR... but maybe we can work around it? + do { + input[inputEnd] = (byte) '}'; + + while (input[inputIndex] != (byte) '}') + inputIndex++; + + if (inputIndex < inputEnd) + break; + + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + } + + // skip over the '}' + inputIndex++; + + // read until we get a new line... + do { + input[inputEnd] = (byte) '\n'; + + while (input[inputIndex] != (byte) '\n') + inputIndex++; + + if (inputIndex < inputEnd) + break; + + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + + // skip over the '\n' + inputIndex++; + + if (!int.TryParse (builder.ToString (), NumberStyles.None, CultureInfo.InvariantCulture, out literalDataLeft) || literalDataLeft < 0) + return new ImapToken (ImapTokenType.Error, builder.ToString ()); + + Mode = ImapStreamMode.Literal; + + return new ImapToken (ImapTokenType.Literal, literalDataLeft); + } + + internal async Task ReadTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (nextToken != null) { + var token = nextToken; + nextToken = null; + return token; + } + + input[inputEnd] = (byte) '\n'; + + // skip over white space between tokens... + do { + while (IsWhiteSpace (input[inputIndex])) + inputIndex++; + + if (inputIndex < inputEnd) + break; + + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + + input[inputEnd] = (byte) '\n'; + } while (true); + + char c = (char) input[inputIndex]; + + if (c == '"') + return await ReadQuotedStringTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (c == '{') + return await ReadLiteralTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (c == '\\') + return await ReadFlagTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false); + + if (IsAtom (input[inputIndex], specials)) + return await ReadAtomTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false); + + // special character token + inputIndex++; + + return new ImapToken ((ImapTokenType) c, c); + } + + internal Task ReadTokenAsync (bool doAsync, CancellationToken cancellationToken) + { + return ReadTokenAsync (DefaultSpecials, doAsync, cancellationToken); + } + + /// + /// Reads the next available token from the stream. + /// + /// The token. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public ImapToken ReadToken (CancellationToken cancellationToken) + { + return ReadTokenAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously reads the next available token from the stream. + /// + /// The token. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task ReadTokenAsync (CancellationToken cancellationToken) + { + return ReadTokenAsync (true, cancellationToken); + } + + /// + /// Ungets a token. + /// + /// The token. + public void UngetToken (ImapToken token) + { + if (token == null) + throw new ArgumentNullException (nameof (token)); + + nextToken = token; + } + + async Task ReadLineAsync (Stream ostream, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (inputIndex == inputEnd) + await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false); + + unsafe { + fixed (byte* inbuf = input) { + byte* start, inptr, inend; + int offset = inputIndex; + int count; + + start = inbuf + inputIndex; + inend = inbuf + inputEnd; + *inend = (byte) '\n'; + inptr = start; + + // FIXME: use SIMD to optimize this + while (*inptr != (byte) '\n') + inptr++; + + inputIndex = (int) (inptr - inbuf); + count = (int) (inptr - start); + + if (inptr == inend) { + ostream.Write (input, offset, count); + return false; + } + + // consume the '\n' + inputIndex++; + count++; + + ostream.Write (input, offset, count); + + return true; + } + } + } + + /// + /// Reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns true. + /// + /// true, if reading the line is complete, false otherwise. + /// The output stream to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal bool ReadLine (Stream ostream, CancellationToken cancellationToken) + { + return ReadLineAsync (ostream, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns true. + /// + /// true, if reading the line is complete, false otherwise. + /// The output stream to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal Task ReadLineAsync (Stream ostream, CancellationToken cancellationToken) + { + return ReadLineAsync (ostream, true, cancellationToken); + } + + async Task WriteAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + var network = NetworkStream.Get (Stream); + int index = offset; + int left = count; + + while (left > 0) { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; + } + + if (outputIndex == BlockSize) { + // flush the output buffer + if (doAsync) { + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, BlockSize); + } + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + if (doAsync) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (buffer, index, BlockSize); + } + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + Write (buffer, offset, count, CancellationToken.None); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync (buffer, offset, count, true, cancellationToken); + } + + async Task FlushAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + if (doAsync) { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, outputIndex); + Stream.Flush (); + } + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Flush (CancellationToken cancellationToken) + { + FlushAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all output buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + Flush (CancellationToken.None); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + return FlushAsync (true, cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// It is not possible to seek within a . + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + /// + /// Sets the length of the stream. + /// + /// + /// It is not possible to set the length of a . + /// + /// The desired length of the stream in bytes. + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + IsConnected = false; + Stream.Dispose (); + } + + disposed = true; + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/Imap/ImapToken.cs b/src/MailKit/Net/Imap/ImapToken.cs new file mode 100644 index 0000000..d67e229 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapToken.cs @@ -0,0 +1,80 @@ +// +// ImapToken.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Globalization; + +namespace MailKit.Net.Imap { + enum ImapTokenType { + NoData = -7, + Error = -6, + Nil = -5, + Atom = -4, + Flag = -3, + QString = -2, + Literal = -1, + + // character tokens: + Eoln = (int) '\n', + OpenParen = (int) '(', + CloseParen = (int) ')', + Asterisk = (int) '*', + OpenBracket = (int) '[', + CloseBracket = (int) ']', + } + + class ImapToken + { + public readonly ImapTokenType Type; + public readonly object Value; + + public ImapToken (ImapTokenType type, object value = null) + { + Value = value; + Type = type; + + //System.Console.WriteLine ("token: {0}", this); + } + + public override string ToString () + { + switch (Type) { + case ImapTokenType.NoData: return ""; + case ImapTokenType.Nil: return "NIL"; + case ImapTokenType.Atom: return "[atom: " + (string) Value + "]"; + case ImapTokenType.Flag: return "[flag: " + (string) Value + "]"; + case ImapTokenType.QString: return "[qstring: \"" + (string) Value + "\"]"; + case ImapTokenType.Literal: return "{" + (int) Value + "}"; + case ImapTokenType.Eoln: return "'\\n'"; + case ImapTokenType.OpenParen: return "'('"; + case ImapTokenType.CloseParen: return "')'"; + case ImapTokenType.Asterisk: return "'*'"; + case ImapTokenType.OpenBracket: return "'['"; + case ImapTokenType.CloseBracket: return "']'"; + default: return string.Format (CultureInfo.InvariantCulture, "[{0}: '{1}']", Type, Value); + } + } + } +} diff --git a/src/MailKit/Net/Imap/ImapUtils.cs b/src/MailKit/Net/Imap/ImapUtils.cs new file mode 100644 index 0000000..aec0733 --- /dev/null +++ b/src/MailKit/Net/Imap/ImapUtils.cs @@ -0,0 +1,1670 @@ +// +// ImapUtils.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +using MimeKit; +using MimeKit.Utils; + +namespace MailKit.Net.Imap { + /// + /// IMAP utility functions. + /// + static class ImapUtils + { + const FolderAttributes SpecialUseAttributes = FolderAttributes.All | FolderAttributes.Archive | FolderAttributes.Drafts | + FolderAttributes.Flagged | FolderAttributes.Important | FolderAttributes.Inbox | FolderAttributes.Junk | + FolderAttributes.Sent | FolderAttributes.Trash; + const string QuotedSpecials = " \t()<>@,;:\\\"/[]?="; + static readonly int InboxLength = "INBOX".Length; + + static readonly string[] Months = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + /// + /// Formats a date in a format suitable for use with the APPEND command. + /// + /// The formatted date string. + /// The date. + public static string FormatInternalDate (DateTimeOffset date) + { + return string.Format (CultureInfo.InvariantCulture, "{0:D2}-{1}-{2:D4} {3:D2}:{4:D2}:{5:D2} {6:+00;-00}{7:00}", + date.Day, Months[date.Month - 1], date.Year, date.Hour, date.Minute, date.Second, + date.Offset.Hours, date.Offset.Minutes); + } + + class UniqueHeaderSet : HashSet + { + public UniqueHeaderSet () : base (StringComparer.Ordinal) + { + } + } + + public static HashSet GetUniqueHeaders (IEnumerable headers) + { + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + // check if this list of headers is already unique (e.g. created by GetUniqueHeaders (IEnumerable)) + if (headers is UniqueHeaderSet unique) + return unique; + + var hash = new UniqueHeaderSet (); + + foreach (var header in headers) { + if (header.Length == 0) + throw new ArgumentException ($"Invalid header field: {header}", nameof (headers)); + + for (int i = 0; i < header.Length; i++) { + char c = header[i]; + + if (c <= 32 || c >= 127 || c == ':') + throw new ArgumentException ($"Illegal characters in header field: {header}", nameof (headers)); + } + + hash.Add (header.ToUpperInvariant ()); + } + + return hash; + } + + public static HashSet GetUniqueHeaders (IEnumerable headers) + { + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + var hash = new UniqueHeaderSet (); + + foreach (var header in headers) { + if (header == HeaderId.Unknown) + continue; + + hash.Add (header.ToHeaderName ().ToUpperInvariant ()); + } + + return hash; + } + + static bool TryGetInt32 (string text, ref int index, out int value) + { + int startIndex = index; + + value = 0; + + while (index < text.Length && text[index] >= '0' && text[index] <= '9') { + int digit = text[index] - '0'; + + if (value > int.MaxValue / 10 || (value == int.MaxValue / 10 && digit > int.MaxValue % 10)) { + // integer overflow + return false; + } + + value = (value * 10) + digit; + index++; + } + + return index > startIndex; + } + + static bool TryGetInt32 (string text, ref int index, char delim, out int value) + { + return TryGetInt32 (text, ref index, out value) && index < text.Length && text[index] == delim; + } + + static bool TryGetMonth (string text, ref int index, char delim, out int month) + { + int startIndex = index; + + month = 0; + + if ((index = text.IndexOf (delim, index)) == -1 || (index - startIndex) != 3) + return false; + + for (int i = 0; i < Months.Length; i++) { + if (string.Compare (Months[i], 0, text, startIndex, 3, StringComparison.OrdinalIgnoreCase) == 0) { + month = i + 1; + return true; + } + } + + return false; + } + + static bool TryGetTimeZone (string text, ref int index, out TimeSpan timezone) + { + int tzone, sign = 1; + + if (text[index] == '-') { + sign = -1; + index++; + } else if (text[index] == '+') { + index++; + } + + if (!TryGetInt32 (text, ref index, out tzone)) { + timezone = new TimeSpan (); + return false; + } + + tzone *= sign; + + while (tzone < -1400) + tzone += 2400; + + while (tzone > 1400) + tzone -= 2400; + + int minutes = tzone % 100; + int hours = tzone / 100; + + timezone = new TimeSpan (hours, minutes, 0); + + return true; + } + + static Exception InvalidInternalDateFormat (string text) + { + return new FormatException ("Invalid INTERNALDATE format: " + text); + } + + /// + /// Parses the internal date string. + /// + /// The date. + /// The text to parse. + public static DateTimeOffset ParseInternalDate (string text) + { + int day, month, year, hour, minute, second; + TimeSpan timezone; + int index = 0; + + while (index < text.Length && char.IsWhiteSpace (text[index])) + index++; + + if (index >= text.Length || !TryGetInt32 (text, ref index, '-', out day) || day < 1 || day > 31) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetMonth (text, ref index, '-', out month)) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out year) || year < 1969) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out hour) || hour > 23) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetInt32 (text, ref index, ':', out minute) || minute > 59) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetInt32 (text, ref index, ' ', out second) || second > 59) + throw InvalidInternalDateFormat (text); + + index++; + if (index >= text.Length || !TryGetTimeZone (text, ref index, out timezone)) + throw InvalidInternalDateFormat (text); + + while (index < text.Length && char.IsWhiteSpace (text[index])) + index++; + + if (index < text.Length) + throw InvalidInternalDateFormat (text); + + // return DateTimeOffset.ParseExact (text.Trim (), "d-MMM-yyyy HH:mm:ss zzz", CultureInfo.InvariantCulture.DateTimeFormat); + return new DateTimeOffset (year, month, day, hour, minute, second, timezone); + } + + /// + /// Formats a list of annotations for a STORE or APPEND command. + /// + /// The command builder. + /// The annotations. + /// the argument list. + /// Throw an exception if there are any annotations without properties. + public static void FormatAnnotations (StringBuilder command, IList annotations, List args, bool throwOnError) + { + int length = command.Length; + int added = 0; + + command.Append ("ANNOTATION ("); + + for (int i = 0; i < annotations.Count; i++) { + var annotation = annotations[i]; + + if (annotation.Properties.Count == 0) { + if (throwOnError) + throw new ArgumentException ("One or more annotations does not define any attributes.", nameof (annotations)); + + continue; + } + + command.Append (annotation.Entry); + command.Append (" ("); + + foreach (var property in annotation.Properties) { + command.AppendFormat ("{0} %S ", property.Key); + args.Add (property.Value); + } + + command[command.Length - 1] = ')'; + command.Append (' '); + + added++; + } + + if (added > 0) + command[command.Length - 1] = ')'; + else + command.Length = length; + } + + /// + /// Formats the array of indexes as a string suitable for use with IMAP commands. + /// + /// The index set. + /// The indexes. + /// + /// is null. + /// + /// + /// One or more of the indexes has a negative value. + /// + public static string FormatIndexSet (IList indexes) + { + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + throw new ArgumentException ("No indexes were specified.", nameof (indexes)); + + var builder = new StringBuilder (); + int index = 0; + + while (index < indexes.Count) { + if (indexes[index] < 0) + throw new ArgumentException ("One or more of the indexes is negative.", nameof (indexes)); + + int begin = indexes[index]; + int end = indexes[index]; + int i = index + 1; + + if (i < indexes.Count) { + if (indexes[i] == end + 1) { + end = indexes[i++]; + + while (i < indexes.Count && indexes[i] == end + 1) { + end++; + i++; + } + } else if (indexes[i] == end - 1) { + end = indexes[i++]; + + while (i < indexes.Count && indexes[i] == end - 1) { + end--; + i++; + } + } + } + + if (builder.Length > 0) + builder.Append (','); + + if (begin != end) + builder.AppendFormat (CultureInfo.InvariantCulture, "{0}:{1}", begin + 1, end + 1); + else + builder.Append ((begin + 1).ToString (CultureInfo.InvariantCulture)); + + index = i; + } + + return builder.ToString (); + } + + /// + /// Parses an untagged ID response. + /// + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static async Task ParseImplementationAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); + var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + ImapImplementation implementation; + + if (token.Type == ImapTokenType.Nil) + return; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + + implementation = new ImapImplementation (); + + while (token.Type != ImapTokenType.CloseParen) { + var property = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + var value = await ReadNStringTokenAsync (engine, format, false, doAsync, ic.CancellationToken).ConfigureAwait (false); + + implementation.Properties[property] = value; + + token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + ic.UserData = implementation; + + // read the ')' token + await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + } + + /// + /// Canonicalize the name of the mailbox. + /// + /// + /// Canonicalizes the name of the mailbox by replacing various + /// capitalizations of "INBOX" with the literal "INBOX" string. + /// + /// The mailbox name. + /// The encoded mailbox name. + /// The directory separator. + public static string CanonicalizeMailboxName (string mailboxName, char directorySeparator) + { + if (!mailboxName.StartsWith ("INBOX", StringComparison.OrdinalIgnoreCase)) + return mailboxName; + + if (mailboxName.Length > InboxLength && mailboxName[InboxLength] == directorySeparator) + return "INBOX" + mailboxName.Substring (InboxLength); + + if (mailboxName.Length == InboxLength) + return "INBOX"; + + return mailboxName; + } + + /// + /// Determines whether the specified mailbox is the Inbox. + /// + /// true if the specified mailbox name is the Inbox; otherwise, false. + /// The mailbox name. + public static bool IsInbox (string mailboxName) + { + return string.Compare (mailboxName, "INBOX", StringComparison.OrdinalIgnoreCase) == 0; + } + + static async Task ReadFolderNameAsync (ImapEngine engine, char delim, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + string encodedName; + + switch (token.Type) { + case ImapTokenType.Literal: + encodedName = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + encodedName = (string) token.Value; + + // Note: Exchange apparently doesn't quote folder names that contain tabs. + // + // See https://github.com/jstedfast/MailKit/issues/945 for details. + if (engine.QuirksMode == ImapQuirksMode.Exchange) { + var line = await engine.ReadLineAsync (doAsync, cancellationToken); + int eoln = line.IndexOf ("\r\n", StringComparison.Ordinal); + eoln = eoln != -1 ? eoln : line.Length - 1; + + // unget the \r\n sequence + token = new ImapToken (ImapTokenType.Eoln); + engine.Stream.UngetToken (token); + + if (eoln > 0) + encodedName += line.Substring (0, eoln); + } + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + return "NIL"; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + return encodedName.TrimEnd (delim); + } + + /// + /// Parses an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The list of folders to be populated. + /// true if it is an LSUB response; otherwise, false. + /// true if the LIST response is expected to return \Subscribed flags; otherwise, false. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task ParseFolderListAsync (ImapEngine engine, List list, bool isLsub, bool returnsSubscribed, bool doAsync, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, isLsub ? "LSUB" : "LIST", "{0}"); + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var attrs = FolderAttributes.None; + ImapFolder folder = null; + string encodedName; + char delim; + + // parse the folder attributes list + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + switch (atom) { + case "\\NoInferiors": attrs |= FolderAttributes.NoInferiors; break; + case "\\Noselect": attrs |= FolderAttributes.NoSelect; break; + case "\\Marked": attrs |= FolderAttributes.Marked; break; + case "\\Unmarked": attrs |= FolderAttributes.Unmarked; break; + case "\\NonExistent": attrs |= FolderAttributes.NonExistent; break; + case "\\Subscribed": attrs |= FolderAttributes.Subscribed; break; + case "\\Remote": attrs |= FolderAttributes.Remote; break; + case "\\HasChildren": attrs |= FolderAttributes.HasChildren; break; + case "\\HasNoChildren": attrs |= FolderAttributes.HasNoChildren; break; + case "\\All": attrs |= FolderAttributes.All; break; + case "\\Archive": attrs |= FolderAttributes.Archive; break; + case "\\Drafts": attrs |= FolderAttributes.Drafts; break; + case "\\Flagged": attrs |= FolderAttributes.Flagged; break; + case "\\Important": attrs |= FolderAttributes.Important; break; + case "\\Junk": attrs |= FolderAttributes.Junk; break; + case "\\Sent": attrs |= FolderAttributes.Sent; break; + case "\\Trash": attrs |= FolderAttributes.Trash; break; + // XLIST flags: + case "\\AllMail": attrs |= FolderAttributes.All; break; + case "\\Inbox": attrs |= FolderAttributes.Inbox; break; + case "\\Spam": attrs |= FolderAttributes.Junk; break; + case "\\Starred": attrs |= FolderAttributes.Flagged; break; + } + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + // parse the path delimeter + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.QString) { + var qstring = (string) token.Value; + + delim = qstring[0]; + } else if (token.Type == ImapTokenType.Nil) { + delim = '\0'; + } else { + throw ImapEngine.UnexpectedToken (format, token); + } + + encodedName = await ReadFolderNameAsync (engine, delim, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (IsInbox (encodedName)) + attrs |= FolderAttributes.Inbox; + + // peek at the next token to see if we have a LIST extension + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + var renamed = false; + + // read the '(' token + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // a LIST extension + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, format, token); + + var atom = (string) token.Value; + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + engine.Stream.UngetToken (token); + + if (!renamed && atom.Equals ("OLDNAME", StringComparison.OrdinalIgnoreCase)) { + var oldEncodedName = await ReadFolderNameAsync (engine, delim, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (engine.FolderCache.TryGetValue (oldEncodedName, out ImapFolder oldFolder)) { + var args = new ImapFolderConstructorArgs (engine, encodedName, attrs, delim); + + engine.FolderCache.Remove (oldEncodedName); + engine.FolderCache[encodedName] = oldFolder; + oldFolder.OnRenamed (args); + folder = oldFolder; + } + + renamed = true; + } else { + await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + } + } while (true); + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Eoln, format, token); + } + + if (folder != null || engine.GetCachedFolder (encodedName, out folder)) { + if ((attrs & FolderAttributes.NonExistent) != 0) { + folder.UpdatePermanentFlags (MessageFlags.None); + folder.UpdateAcceptedFlags (MessageFlags.None); + folder.UpdateUidNext (UniqueId.Invalid); + folder.UpdateHighestModSeq (0); + folder.UpdateUidValidity (0); + folder.UpdateUnread (0); + } + + if (isLsub) { + // Note: merge all pre-existing attributes since the LSUB response will not contain them + attrs |= folder.Attributes | FolderAttributes.Subscribed; + } else { + // Note: only merge the SPECIAL-USE and \Subscribed attributes for a LIST command + attrs |= folder.Attributes & SpecialUseAttributes; + + // Note: only merge \Subscribed if the LIST command isn't expected to include it + if (!returnsSubscribed) + attrs |= folder.Attributes & FolderAttributes.Subscribed; + } + + folder.UpdateAttributes (attrs); + } else { + folder = engine.CreateImapFolder (encodedName, attrs, delim); + engine.CacheFolder (folder); + + if (list == null) + engine.OnFolderCreated (folder); + } + + // Note: list will be null if this is an unsolicited LIST response due to an active NOTIFY request + list?.Add (folder); + } + + /// + /// Parses an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task ParseFolderListAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var list = (List) ic.UserData; + + return ParseFolderListAsync (engine, list, ic.Lsub, ic.ListReturnsSubscribed, doAsync, ic.CancellationToken); + } + + /// + /// Parses an untagged METADATA response. + /// + /// The encoded name of the folder that the metadata belongs to. + /// The IMAP engine. + /// The metadata collection to be populated. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task ParseMetadataAsync (ImapEngine engine, MetadataCollection metadata, bool doAsync, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); + var encodedName = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + var tag = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var value = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + metadata.Add (new Metadata (MetadataTag.Create (tag), value) { EncodedName = encodedName }); + + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + // read the closing paren + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + /// + /// Parses an untagged METADATA response. + /// + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task ParseMetadataAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var metadata = (MetadataCollection) ic.UserData; + + return ParseMetadataAsync (engine, metadata, doAsync, ic.CancellationToken); + } + + internal static async Task ReadStringTokenAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.Literal: + return await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + case ImapTokenType.QString: + case ImapTokenType.Atom: + return (string) token.Value; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + static async Task ReadNStringTokenAsync (ImapEngine engine, string format, bool rfc2047, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + string value; + + switch (token.Type) { + case ImapTokenType.Literal: + value = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + value = (string) token.Value; + break; + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + if (rfc2047) { + var encoding = engine.UTF8Enabled ? ImapEngine.UTF8 : ImapEngine.Latin1; + + return Rfc2047.DecodeText (encoding.GetBytes (value)); + } + + return value; + } + + static async Task ReadNumberAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // Note: this is a work-around for broken IMAP servers that return negative integer values for things + // like octet counts and line counts. + if (token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + if (atom.Length > 0 && atom[0] == '-') { + if (!int.TryParse (atom, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var negative)) + throw ImapEngine.UnexpectedToken (format, token); + + // Note: since Octets & Lines are the only 2 values this method is responsible for parsing, + // it seems the only sane value to return would be 0. + return 0; + } + } + + return ImapEngine.ParseNumber (token, false, format, token); + } + + static bool NeedsQuoting (string value) + { + for (int i = 0; i < value.Length; i++) { + if (value[i] > 127 || char.IsControl (value[i])) + return true; + + if (QuotedSpecials.IndexOf (value[i]) != -1) + return true; + } + + return value.Length == 0; + } + + static async Task ParseParameterListAsync (StringBuilder builder, ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + // Note: technically, the value should also be a 'string' token and not an 'nstring', + // but issue #124 reveals a server that is sending NIL for boundary values. + var value = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false) ?? string.Empty; + + builder.Append ("; ").Append (name).Append ('='); + + if (NeedsQuoting (value)) + builder.Append (MimeUtils.Quote (value)); + else + builder.Append (value); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + static async Task ParseContentTypeAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var type = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false) ?? "application"; + var token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + ContentType contentType; + string subtype; + + if (token.Type == ImapTokenType.OpenParen || token.Type == ImapTokenType.Nil) { + // Note: work around broken IMAP server implementations... + if (engine.QuirksMode == ImapQuirksMode.GMail) { + // Note: GMail's IMAP server implementation breaks when it encounters + // nested multiparts with the same boundary and returns a BODYSTRUCTURE + // like the example in https://github.com/jstedfast/MailKit/issues/205 or + // like the example in https://github.com/jstedfast/MailKit/issues/777 + // + // Note: this token is either '(' to start the Content-Type parameter values + // or it is a NIL to specify that there are no parameter values. + return type; + } + + if (token.Type != ImapTokenType.Nil) { + // Note: In other IMAP server implementations, such as the one found in + // https://github.com/jstedfast/MailKit/issues/371, if the server comes + // across something like "Content-Type: X-ZIP", it will only send a + // media-subtype token and completely fail to send a media-type token. + subtype = type; + type = "application"; + } else { + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + subtype = string.Empty; + } + } else { + subtype = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + } + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return new ContentType (type, subtype); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var builder = new StringBuilder (); + builder.AppendFormat ("{0}/{1}", type, subtype); + + await ParseParameterListAsync (builder, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (!ContentType.TryParse (builder.ToString (), out contentType)) + contentType = new ContentType (type, subtype); + + return contentType; + } + + static async Task ParseContentDispositionAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + // body-fld-dsp = "(" string SP body-fld-param ")" / nil + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return null; + + if (token.Type != ImapTokenType.OpenParen) { + // Note: this is a work-around for issue #919 where Exchange sends `"inline"` instead of `("inline" NIL)` + if (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) + return new ContentDisposition ((string) token.Value); + + throw ImapEngine.UnexpectedToken (format, token); + } + + // Exchange bug: ... (NIL NIL) ... + var dsp = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + var builder = new StringBuilder (); + ContentDisposition disposition; + bool isNil = false; + + // Note: These are work-arounds for some bugs in some mail clients that + // either leave out the disposition value or quote it. + // + // See https://github.com/jstedfast/MailKit/issues/486 for details. + if (string.IsNullOrEmpty (dsp)) + builder.Append (ContentDisposition.Attachment); + else + builder.Append (dsp.Trim ('"')); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) + await ParseParameterListAsync (builder, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + else if (token.Type != ImapTokenType.Nil) + throw ImapEngine.UnexpectedToken (format, token); + else + isNil = true; + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + if (dsp == null && isNil) + return null; + + ContentDisposition.TryParse (builder.ToString (), out disposition); + + return disposition; + } + + static async Task ParseContentLanguageAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var languages = new List (); + string language; + + switch (token.Type) { + case ImapTokenType.Literal: + language = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + languages.Add (language); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + language = (string) token.Value; + languages.Add (language); + break; + case ImapTokenType.Nil: + return null; + case ImapTokenType.OpenParen: + do { + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // Note: Some broken IMAP servers send `NIL` tokens in this list. Just ignore them. + // + // See https://github.com/jstedfast/MailKit/issues/953 + language = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + + if (language != null) + languages.Add (language); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + return languages.ToArray (); + } + + static async Task ParseContentLocationAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var location = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + + if (string.IsNullOrWhiteSpace (location)) + return null; + + if (Uri.IsWellFormedUriString (location, UriKind.Absolute)) + return new Uri (location, UriKind.Absolute); + + if (Uri.IsWellFormedUriString (location, UriKind.Relative)) + return new Uri (location, UriKind.Relative); + + return null; + } + + static async Task SkipBodyExtensionAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.OpenParen: + do { + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + await SkipBodyExtensionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + } while (true); + + // read the ')' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.Literal: + await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + case ImapTokenType.Nil: + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + } + + static async Task ParseMultipartAsync (ImapEngine engine, string format, string path, string subtype, bool doAsync, CancellationToken cancellationToken) + { + var prefix = path.Length > 0 ? path + "." : string.Empty; + var body = new BodyPartMultipart (); + ImapToken token; + int index = 1; + + // Note: if subtype is not null, then we are working around a GMail bug... + if (subtype == null) { + do { + body.BodyParts.Add (await ParseBodyAsync (engine, format, prefix + index, doAsync, cancellationToken).ConfigureAwait (false)); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + index++; + } while (token.Type == ImapTokenType.OpenParen); + + subtype = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + } + + body.ContentType = new ContentType ("multipart", subtype); + body.PartSpecifier = path; + + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type != ImapTokenType.CloseParen) { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapTokenType.Nil, format, token); + + var builder = new StringBuilder (); + ContentType contentType; + + builder.AppendFormat ("{0}/{1}", body.ContentType.MediaType, body.ContentType.MediaSubtype); + + if (token.Type == ImapTokenType.OpenParen) + await ParseParameterListAsync (builder, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (ContentType.TryParse (builder.ToString (), out contentType)) + body.ContentType = contentType; + + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type == ImapTokenType.QString) { + // Note: This is a work-around for broken Exchange servers. + // + // See https://stackoverflow.com/questions/33481604/mailkit-fetch-unexpected-token-in-imap-response-qstring-multipart-message + // for details. + + // Read what appears to be a Content-Description. + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // Peek ahead at the next token. It has been suggested that this next token seems to be the Content-Language value. + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } else if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = await ParseContentDispositionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = await ParseContentLanguageAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLocation = await ParseContentLocationAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + while (token.Type != ImapTokenType.CloseParen) { + await SkipBodyExtensionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + // read the ')' + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + return body; + } + + public static async Task ParseBodyAsync (ImapEngine engine, string format, string path, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return null; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // Note: If we immediately get a closing ')', then treat it the same as if we had gotten a `NIL` `body` token. + // + // See https://github.com/jstedfast/MailKit/issues/944 for details. + if (token.Type == ImapTokenType.CloseParen) { + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + return null; + } + + if (token.Type == ImapTokenType.OpenParen) + return await ParseMultipartAsync (engine, format, path, null, doAsync, cancellationToken).ConfigureAwait (false); + + var result = await ParseContentTypeAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (result is string) { + // GMail breakage... yay! What we have is a nested multipart with + // the same boundary as its parent. + return await ParseMultipartAsync (engine, format, path, (string) result, doAsync, cancellationToken).ConfigureAwait (false); + } + + var id = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + var desc = await ReadNStringTokenAsync (engine, format, true, doAsync, cancellationToken).ConfigureAwait (false); + // Note: technically, body-fld-enc, is not allowed to be NIL, but we need to deal with broken servers... + var enc = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + var octets = await ReadNumberAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var type = (ContentType) result; + var isMultipart = false; + BodyPartBasic body; + + if (type.IsMimeType ("message", "rfc822")) { + var mesg = new BodyPartMessage (); + + // Note: GMail (and potentially other IMAP servers) will send body-part-basic + // expressions instead of body-part-msg expressions when they encounter + // message/rfc822 MIME parts that are illegally encoded using base64 (or + // quoted-printable?). According to rfc3501, IMAP servers are REQUIRED to + // send body-part-msg expressions for message/rfc822 parts, however, it is + // understandable why GMail (and other IMAP servers?) do what they do in this + // particular case. + // + // For examples, see issue #32 and issue #59. + // + // The workaround is to check for the expected '(' signifying an envelope token. + // If we do not get an '(', then we are likely looking at the Content-MD5 token + // which gets handled below. + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + mesg.Envelope = await ParseEnvelopeAsync (engine, doAsync, cancellationToken).ConfigureAwait (false); + mesg.Body = await ParseBodyAsync (engine, format, path, doAsync, cancellationToken).ConfigureAwait (false); + mesg.Lines = await ReadNumberAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + } + + body = mesg; + } else if (type.IsMimeType ("text", "*")) { + var text = new BodyPartText (); + text.Lines = await ReadNumberAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + body = text; + } else { + isMultipart = type.IsMimeType ("multipart", "*"); + body = new BodyPartBasic (); + } + + body.ContentTransferEncoding = enc; + body.ContentDescription = desc; + body.PartSpecifier = path; + body.ContentType = type; + body.ContentId = id; + body.Octets = octets; + + // if we are parsing a BODYSTRUCTURE, we may get some more tokens before the ')' + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (!isMultipart) { + if (token.Type != ImapTokenType.CloseParen) { + body.ContentMd5 = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentDisposition = await ParseContentDispositionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLanguage = await ParseContentLanguageAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + if (token.Type != ImapTokenType.CloseParen) { + body.ContentLocation = await ParseContentLocationAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + } + + while (token.Type != ImapTokenType.CloseParen) { + await SkipBodyExtensionAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + // read the ')' + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + return body; + } + + struct EnvelopeAddress + { + public readonly string Name; + public readonly string Route; + public readonly string Mailbox; + public readonly string Domain; + + public EnvelopeAddress (string[] values) + { + Name = values[0]; + Route = values[1]; + Mailbox = values[2]; + Domain = values[3]; + } + + public bool IsGroupStart { + get { return Name == null && Route == null && Mailbox != null && Domain == null; } + } + + public bool IsGroupEnd { + get { return Name == null && Route == null && Mailbox == null && Domain == null; } + } + + public MailboxAddress ToMailboxAddress (ImapEngine engine) + { + var mailbox = Mailbox; + var domain = Domain; + string name = null; + + if (Name != null) { + var encoding = engine.UTF8Enabled ? ImapEngine.UTF8 : ImapEngine.Latin1; + + name = Rfc2047.DecodePhrase (encoding.GetBytes (Name)); + } + + // Note: When parsing mailbox addresses w/o a domain, Dovecot will + // use "MISSING_DOMAIN" as the domain string to prevent it from + // appearing as a group address in the IMAP ENVELOPE response. + if (domain == "MISSING_DOMAIN" || domain == ".MISSING-HOST-NAME.") + domain = null; + else if (domain != null) + domain = domain.TrimEnd ('>'); + + if (mailbox != null) + mailbox = mailbox.TrimStart ('<'); + + string address = domain != null ? mailbox + "@" + domain : mailbox; + DomainList route; + + if (Route != null && DomainList.TryParse (Route, out route)) + return new MailboxAddress (name, route, address); + + return new MailboxAddress (name, address); + } + + public GroupAddress ToGroupAddress (ImapEngine engine) + { + var name = string.Empty; + + if (Mailbox != null) { + var encoding = engine.UTF8Enabled ? ImapEngine.UTF8 : ImapEngine.Latin1; + + name = Rfc2047.DecodePhrase (encoding.GetBytes (Mailbox)); + } + + return new GroupAddress (name); + } + } + + static async Task ParseEnvelopeAddressAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var values = new string[4]; + ImapToken token; + int index = 0; + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + switch (token.Type) { + case ImapTokenType.Literal: + values[index] = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + values[index] = (string) token.Value; + break; + case ImapTokenType.Nil: + break; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + index++; + } while (index < 4); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + return new EnvelopeAddress (values); + } + + static async Task ParseEnvelopeAddressListAsync (InternetAddressList list, ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Nil) + return; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var stack = new List (); + int sp = 0; + + stack.Add (list); + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // Note: As seen in https://github.com/jstedfast/MailKit/issues/991, it seems that SmarterMail IMAP + // servers will sometimes include a NIL address token within the address list. Just ignore it. + if (token.Type == ImapTokenType.Nil) + continue; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var address = await ParseEnvelopeAddressAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + if (address.IsGroupStart && engine.QuirksMode != ImapQuirksMode.GMail) { + var group = address.ToGroupAddress (engine); + stack[sp].Add (group); + stack.Add (group.Members); + sp++; + } else if (address.IsGroupEnd) { + if (sp > 0) { + stack.RemoveAt (sp); + sp--; + } + } else { + try { + // Note: We need to do a try/catch around ToMailboxAddress() because some addresses + // returned by the IMAP server might be completely horked. For an example, see the + // second error report in https://github.com/jstedfast/MailKit/issues/494 where one + // of the addresses in the ENVELOPE has the name and address tokens flipped. + var mailbox = address.ToMailboxAddress (engine); + stack[sp].Add (mailbox); + } catch { + continue; + } + } + } while (true); + } + + static async Task ParseEnvelopeDateAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + DateTimeOffset date; + string value; + + switch (token.Type) { + case ImapTokenType.Literal: + value = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + value = (string) token.Value; + break; + case ImapTokenType.Nil: + return null; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + if (!DateUtils.TryParse (value, out date)) + return null; + + return date; + } + + /// + /// Parses the ENVELOPE parenthesized list. + /// + /// The envelope. + /// The IMAP engine. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task ParseEnvelopeAsync (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + string format = string.Format (ImapEngine.GenericItemSyntaxErrorFormat, "ENVELOPE", "{0}"); + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + string nstring; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + var envelope = new Envelope (); + envelope.Date = await ParseEnvelopeDateAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + envelope.Subject = await ReadNStringTokenAsync (engine, format, true, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.From, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Sender, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.ReplyTo, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.To, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Cc, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + await ParseEnvelopeAddressListAsync (envelope.Bcc, engine, format, doAsync, cancellationToken).ConfigureAwait (false); + + // Note: Some broken IMAP servers will forget to include the In-Reply-To token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/932 + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false)) != null) + envelope.InReplyTo = MimeUtils.EnumerateReferences (nstring).FirstOrDefault (); + + // Note: Some broken IMAP servers will forget to include the Message-Id token (I guess if the header isn't set?). + // + // See https://github.com/jstedfast/MailKit/issues/669 + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + if (token.Type != ImapTokenType.CloseParen) { + if ((nstring = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false)) != null) { + try { + envelope.MessageId = MimeUtils.ParseMessageId (nstring); + } catch { + envelope.MessageId = nstring; + } + } + } + } + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + return envelope; + } + + /// + /// Formats a flags list suitable for use with the APPEND command. + /// + /// The flags list string. + /// The message flags. + /// The number of keywords. + public static string FormatFlagsList (MessageFlags flags, int numKeywords) + { + var builder = new StringBuilder (); + + builder.Append ('('); + + if ((flags & MessageFlags.Answered) != 0) + builder.Append ("\\Answered "); + if ((flags & MessageFlags.Deleted) != 0) + builder.Append ("\\Deleted "); + if ((flags & MessageFlags.Draft) != 0) + builder.Append ("\\Draft "); + if ((flags & MessageFlags.Flagged) != 0) + builder.Append ("\\Flagged "); + if ((flags & MessageFlags.Seen) != 0) + builder.Append ("\\Seen "); + + for (int i = 0; i < numKeywords; i++) + builder.Append ("%S "); + + if (builder.Length > 1) + builder.Length--; + + builder.Append (')'); + + return builder.ToString (); + } + + /// + /// Parses the flags list. + /// + /// The message flags. + /// The IMAP engine. + /// The name of the flags being parsed. + /// A hash set of user-defined message flags that will be populated if non-null. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task ParseFlagsListAsync (ImapEngine engine, string name, HashSet keywords, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var flags = MessageFlags.None; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { + if (token.Type != ImapTokenType.Nil) { + var flag = (string) token.Value; + + switch (flag) { + case "\\Answered": flags |= MessageFlags.Answered; break; + case "\\Deleted": flags |= MessageFlags.Deleted; break; + case "\\Draft": flags |= MessageFlags.Draft; break; + case "\\Flagged": flags |= MessageFlags.Flagged; break; + case "\\Seen": flags |= MessageFlags.Seen; break; + case "\\Recent": flags |= MessageFlags.Recent; break; + case "\\*": flags |= MessageFlags.UserDefined; break; + default: + if (keywords != null) + keywords.Add (flag); + break; + } + } + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, name, token); + + return flags; + } + + /// + /// Parses the ANNOTATION list. + /// + /// The list of annotations. + /// The IMAP engine. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task> ParseAnnotationsAsync (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ANNOTATION", "{0}"); + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var annotations = new List (); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "ANNOTATION", token); + + do { + token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var path = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var entry = AnnotationEntry.Parse (path); + var annotation = new Annotation (entry); + + annotations.Add (annotation); + + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // Note: Unsolicited FETCH responses that include ANNOTATION data do not include attribute values. + if (token.Type == ImapTokenType.OpenParen) { + // consume the '(' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + // read the attribute/value pairs + do { + token = await engine.PeekTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var name = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var value = await ReadNStringTokenAsync (engine, format, false, doAsync, cancellationToken).ConfigureAwait (false); + var attribute = new AnnotationAttribute (name); + + annotation.Properties[attribute] = value; + } while (true); + + // consume the ')' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + } while (true); + + // consume the ')' + await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + return new ReadOnlyCollection (annotations); + } + + /// + /// Parses the X-GM-LABELS list. + /// + /// The message labels. + /// The IMAP engine. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task> ParseLabelsListAsync (ImapEngine engine, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var labels = new List (); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString || token.Type == ImapTokenType.Nil) { + // Apparently it's possible to set a NIL label in GMail... + // + // See https://github.com/jstedfast/MailKit/issues/244 for an example. + if (token.Type != ImapTokenType.Nil) { + var label = engine.DecodeMailboxName ((string) token.Value); + + labels.Add (label); + } else { + labels.Add ("NIL"); + } + + token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, ImapEngine.GenericItemSyntaxErrorFormat, "X-GM-LABELS", token); + + return new ReadOnlyCollection (labels); + } + + static async Task ParseThreadAsync (ImapEngine engine, uint uidValidity, bool doAsync, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + MessageThread thread, node, child; + uint uid; + + if (token.Type == ImapTokenType.OpenParen) { + thread = new MessageThread ((UniqueId?) null /*UniqueId.Invalid*/); + + do { + child = await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false); + thread.Children.Add (child); + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.CloseParen); + + return thread; + } + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + node = thread = new MessageThread (new UniqueId (uidValidity, uid)); + + do { + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + if (token.Type == ImapTokenType.OpenParen) { + child = await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false); + node.Children.Add (child); + } else { + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + child = new MessageThread (new UniqueId (uidValidity, uid)); + node.Children.Add (child); + node = child; + } + } while (true); + + return thread; + } + + /// + /// Parses the threads. + /// + /// The threads. + /// The IMAP engine. + /// The UIDVALIDITY of the folder. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public static async Task> ParseThreadsAsync (ImapEngine engine, uint uidValidity, bool doAsync, CancellationToken cancellationToken) + { + var threads = new List (); + ImapToken token; + + do { + token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.Eoln) + break; + + token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + + threads.Add (await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false)); + } while (true); + + return threads; + } + } +} diff --git a/src/MailKit/Net/NetworkStream.cs b/src/MailKit/Net/NetworkStream.cs new file mode 100644 index 0000000..6a40302 --- /dev/null +++ b/src/MailKit/Net/NetworkStream.cs @@ -0,0 +1,298 @@ +// +// NetworkStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net +{ + class NetworkStream : Stream + { + SocketAsyncEventArgs send; + SocketAsyncEventArgs recv; + bool ownsSocket; + bool connected; + + public NetworkStream (Socket socket, bool ownsSocket) + { + send = new SocketAsyncEventArgs (); + send.Completed += AsyncOperationCompleted; + send.AcceptSocket = socket; + + recv = new SocketAsyncEventArgs (); + recv.Completed += AsyncOperationCompleted; + recv.AcceptSocket = socket; + + this.ownsSocket = ownsSocket; + connected = socket.Connected; + Socket = socket; + } + + ~NetworkStream () + { + Dispose (false); + } + + public Socket Socket { + get; private set; + } + + public bool DataAvailable { + get { return connected && Socket.Available > 0; } + } + + public override bool CanRead { + get { return connected; } + } + + public override bool CanWrite { + get { return connected; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanTimeout { + get { return connected; } + } + + public override long Length { + get { throw new NotSupportedException (); } + } + + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + public override int ReadTimeout { + get { + int timeout = Socket.ReceiveTimeout; + + return timeout == 0 ? Timeout.Infinite : timeout; + } + set { + if (value <= 0 && value != Timeout.Infinite) + throw new ArgumentOutOfRangeException (nameof (value)); + + Socket.ReceiveTimeout = value; + } + } + + public override int WriteTimeout { + get { + int timeout = Socket.SendTimeout; + + return timeout == 0 ? Timeout.Infinite : timeout; + } + set { + if (value <= 0 && value != Timeout.Infinite) + throw new ArgumentOutOfRangeException (nameof (value)); + + Socket.SendTimeout = value; + } + } + + void AsyncOperationCompleted (object sender, SocketAsyncEventArgs args) + { + var tcs = (TaskCompletionSource) args.UserToken; + + if (args.SocketError == SocketError.Success) { + tcs.TrySetResult (true); + return; + } + + tcs.TrySetException (new SocketException ((int) args.SocketError)); + } + + void Disconnect () + { + try { + Socket.Dispose (); + } catch { + return; + } finally { + connected = false; + send.Dispose (); + send = null; + recv.Dispose (); + recv = null; + } + } + + public override int Read (byte[] buffer, int offset, int count) + { + try { + return Socket.Receive (buffer, offset, count, SocketFlags.None); + } catch (SocketException ex) { + throw new IOException (ex.Message, ex); + } + } + + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var tcs = new TaskCompletionSource (); + + using (var timeout = new CancellationTokenSource (ReadTimeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, timeout.Token)) { + using (var registration = linked.Token.Register (() => tcs.TrySetCanceled (), false)) { + recv.SetBuffer (buffer, offset, count); + recv.UserToken = tcs; + + if (!Socket.ReceiveAsync (recv)) + AsyncOperationCompleted (null, recv); + + try { + await tcs.Task.ConfigureAwait (false); + return recv.BytesTransferred; + } catch (OperationCanceledException) { + if (Socket.Connected) + Socket.Shutdown (SocketShutdown.Both); + + Disconnect (); + throw; + } catch (Exception ex) { + Disconnect (); + if (ex is SocketException) + throw new IOException (ex.Message, ex); + throw; + } + } + } + } + } + + public override void Write (byte[] buffer, int offset, int count) + { + try { + Socket.Send (buffer, offset, count, SocketFlags.None); + } catch (SocketException ex) { + throw new IOException (ex.Message, ex); + } + } + + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var tcs = new TaskCompletionSource (); + + using (var timeout = new CancellationTokenSource (WriteTimeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, timeout.Token)) { + using (var registration = linked.Token.Register (() => tcs.TrySetCanceled (), false)) { + send.SetBuffer (buffer, offset, count); + send.UserToken = tcs; + + if (!Socket.SendAsync (send)) + AsyncOperationCompleted (null, send); + + try { + await tcs.Task.ConfigureAwait (false); + } catch (OperationCanceledException) { + if (Socket.Connected) + Socket.Shutdown (SocketShutdown.Both); + + Disconnect (); + throw; + } catch (Exception ex) { + Disconnect (); + if (ex is SocketException) + throw new IOException (ex.Message, ex); + throw; + } + } + } + } + } + + public override void Flush () + { + } + + public override Task FlushAsync (CancellationToken cancellationToken) + { + return Task.FromResult (true); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public static NetworkStream Get (Stream stream) + { + if (stream is CompressedStream compressed) + stream = compressed.InnerStream; + + if (stream is SslStream ssl) + stream = ssl.InnerStream; + + return stream as NetworkStream; + } + + public void Poll (SelectMode mode, CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + return; + + do { + cancellationToken.ThrowIfCancellationRequested (); + // wait 1/4 second and then re-check for cancellation + } while (!Socket.Poll (250000, mode)); + + cancellationToken.ThrowIfCancellationRequested (); + } + + protected override void Dispose (bool disposing) + { + if (disposing) { + if (ownsSocket && connected) { + ownsSocket = false; + Disconnect (); + } else { + send?.Dispose (); + send = null; + + recv?.Dispose (); + recv = null; + } + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/Pop3/AsyncPop3Client.cs b/src/MailKit/Net/Pop3/AsyncPop3Client.cs new file mode 100644 index 0000000..00a16cf --- /dev/null +++ b/src/MailKit/Net/Pop3/AsyncPop3Client.cs @@ -0,0 +1,1496 @@ +// +// AsyncPop3Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Security; + +namespace MailKit.Net.Pop3 +{ + public partial class Pop3Client + { + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (mechanism, true, cancellationToken); + } + + /// + /// Asynchronously authenticates using the supplied credentials. + /// + /// + /// If the POP3 server supports the APOP authentication mechanism, + /// then APOP is used. + /// If the APOP authentication mechanism is not supported and the + /// server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the USER and PASS commands are used as a + /// fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// In the case of the APOP authentication mechanism, remove it from the + /// property instead. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (encoding, credentials, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 995. All other values will use a default port of 110. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (socket, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified POP3 or POP3/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (stream, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is true, a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to true, a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + return DisconnectAsync (quit, true, cancellationToken); + } + + /// + /// Asynchronously get the message count. + /// + /// + /// Asynchronously gets the message count. + /// + /// The message count. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task GetMessageCountAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + await UpdateMessageCountAsync (true, cancellationToken).ConfigureAwait (false); + + return Count; + } + + /// + /// Ping the POP3 server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return NoOpAsync (true, cancellationToken); + } + + /// + /// Asynchronously enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)) + { + return EnableUTF8Async (true, cancellationToken); + } + + /// + /// Asynchronously get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public Task> GetLanguagesAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetLanguagesAsync (true, cancellationToken); + } + + /// + /// Asynchronously set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// An asynchronous task context. + /// The language code. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public Task SetLanguageAsync (string lang, CancellationToken cancellationToken = default (CancellationToken)) + { + return SetLanguageAsync (lang, true, cancellationToken); + } + + /// + /// Asynchronously get the UID of the message at the specified index. + /// + /// + /// Gets the UID of the message at the specified index. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageUidAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageUidAsync (index, true, cancellationToken); + } + + /// + /// Asynchronously get the full list of available message UIDs. + /// + /// + /// Gets the full list of available message UIDs. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// + /// + /// + /// The message uids. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageUidsAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageUidsAsync (true, cancellationToken); + } + + /// + /// Asynchronously get the size of the specified message, in bytes. + /// + /// + /// Gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageSizeAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageSizeAsync (index, true, cancellationToken); + } + + /// + /// Asynchronously get the sizes for all available messages, in bytes. + /// + /// + /// Gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageSizesAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageSizesAsync (true, cancellationToken); + } + + /// + /// Asynchronously get the headers for the message at the specified index. + /// + /// + /// Gets the headers for the message at the specified index. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageHeadersAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (index + 1, true, true, cancellationToken); + } + + /// + /// Asynchronously get the headers for the messages at the specified indexes. + /// + /// + /// Gets the headers for the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageHeadersAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return Task.FromResult ((IList) new HeaderList[0]); + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (seqids, true, true, cancellationToken); + } + + /// + /// Asynchronously get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessageHeadersAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return Task.FromResult ((IList) new HeaderList[0]); + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (seqids, true, true, cancellationToken); + } + + /// + /// Asynchronously get the message at the specified index. + /// + /// + /// Gets the message at the specified index. + /// + /// + /// + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (index + 1, false, true, cancellationToken); + } + + /// + /// Asynchronously get the messages at the specified indexes. + /// + /// + /// Gets the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return Task.FromResult ((IList) new MimeMessage[0]); + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (seqids, false, true, cancellationToken); + } + + /// + /// Asynchronously get the messages within the specified range. + /// + /// + /// Gets the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// + /// + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return Task.FromResult ((IList) new MimeMessage[0]); + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (seqids, false, true, cancellationToken); + } + + /// + /// Asynchronously get the message or header stream at the specified index. + /// + /// + /// Gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task GetStreamAsync (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (index + 1, headersOnly, true, cancellationToken); + } + + /// + /// Asynchronously get the message or header streams at the specified indexes. + /// + /// + /// Get the message or header streams at the specified indexes. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetStreamsAsync (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return Task.FromResult ((IList) new Stream[0]); + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (seqids, headersOnly, true, cancellationToken); + } + + /// + /// Asynchronously get the message or header streams within the specified range. + /// + /// + /// Gets the message or header streams within the specified range. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task> GetStreamsAsync (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return Task.FromResult ((IList) new Stream[0]); + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (seqids, headersOnly, true, cancellationToken); + } + + /// + /// Asynchronously mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// An asynchronous task context. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task DeleteMessageAsync (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + return DeleteMessageAsync (index, true, cancellationToken); + } + + /// + /// Asynchronously mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task DeleteMessagesAsync (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + { + return DeleteMessagesAsync (indexes, true, cancellationToken); + } + + /// + /// Asynchronously mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// An asynchronous task context. + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task DeleteMessagesAsync (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) + { + return DeleteMessagesAsync (startIndex, count, true, cancellationToken); + } + + /// + /// Asynchronously mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override async Task DeleteAllMessagesAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + if (total > 0) + await DeleteMessagesAsync (0, total, cancellationToken).ConfigureAwait (false); + } + + /// + /// Asynchronously reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// An awaitable task. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Task ResetAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return ResetAsync (true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Pop3/IPop3Client.cs b/src/MailKit/Net/Pop3/IPop3Client.cs new file mode 100644 index 0000000..1598288 --- /dev/null +++ b/src/MailKit/Net/Pop3/IPop3Client.cs @@ -0,0 +1,309 @@ +// +// IPop3Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Pop3 { + /// + /// An interface for a POP3 client. + /// + /// + /// Implemented by . + /// + public interface IPop3Client : IMailSpool + { + /// + /// Gets the capabilities supported by the POP3 server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + Pop3Capabilities Capabilities { get; set; } + + /// + /// Gets the expiration policy. + /// + /// + /// If the server supports the EXPIRE capability (), the value + /// of the property will reflect the value advertized by the server. + /// A value of -1 indicates that messages will never expire. + /// A value of 0 indicates that messages that have been retrieved during the current session + /// will be purged immediately after the connection is closed via the QUIT command. + /// Values larger than 0 indicate the minimum number of days that the server will retain + /// messages which have been retrieved. + /// + /// + /// + /// + /// The expiration policy. + int ExpirePolicy { get; } + + /// + /// Gets the implementation details of the server. + /// + /// + /// If the server advertizes its implementation details, this value will be set to a string containing the + /// information details provided by the server. + /// + /// The implementation details. + string Implementation { get; } + + /// + /// Gets the minimum delay, in milliseconds, between logins. + /// + /// + /// If the server supports the LOGIN-DELAY capability (), this value + /// will be set to the minimum number of milliseconds that the client must wait between logins. + /// + /// + /// + /// + /// The login delay. + int LoginDelay { get; } + + /// + /// Enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task EnableUTF8Async (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + IList GetLanguages (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task> GetLanguagesAsync (CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// The language code. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + void SetLanguage (string lang, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// An asynchronous task context. + /// The language code. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + Task SetLanguageAsync (string lang, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Capabilities.cs b/src/MailKit/Net/Pop3/Pop3Capabilities.cs new file mode 100644 index 0000000..168aee8 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Capabilities.cs @@ -0,0 +1,129 @@ +// +// Pop3Capabilities.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net.Pop3 { + /// + /// Capabilities supported by a POP3 server. + /// + /// + /// Capabilities are read as part of the response to the CAPA command that + /// is issued during the connection and authentication phases of the + /// . + /// + /// + /// + /// + [Flags] + public enum Pop3Capabilities : uint { + /// + /// The server does not support any additional extensions. + /// + None = 0, + + /// + /// The server supports APOP + /// authentication. + /// + Apop = 1 << 0, + + /// + /// The server supports the EXPIRE extension + /// and defines the expiration policy for messages (see ). + /// + Expire = 1 << 1, + + /// + /// The server supports the LOGIN-DELAY extension, + /// allowing the server to specify to the client a minimum number of seconds between login attempts + /// (see ). + /// + LoginDelay = 1 << 2, + + /// + /// The server supports the PIPELINING extension, + /// allowing the client to batch multiple requests to the server at at time. + /// + Pipelining = 1 << 3, + + /// + /// The server supports the RESP-CODES extension, + /// allowing the server to provide clients with extended information in error responses. + /// + ResponseCodes = 1 << 4, + + /// + /// The server supports the SASL authentication + /// extension, allowing the client to authenticate using the advertized authentication mechanisms + /// (see ). + /// + Sasl = 1 << 5, + + /// + /// The server supports the STLS extension, + /// allowing clients to switch to an encrypted SSL/TLS connection after connecting. + /// + StartTLS = 1 << 6, + + /// + /// The server supports the TOP command, + /// allowing clients to fetch the headers plus an arbitrary number of lines. + /// + Top = 1 << 7, + + /// + /// The server supports the UIDL command, + /// allowing the client to refer to messages via a UID as opposed to a sequence ID. + /// + UIDL = 1 << 8, + + /// + /// The server supports the USER + /// authentication command, allowing the client to authenticate via a plain-text username + /// and password command (not recommended unless no other authentication mechanisms exist). + /// + User = 1 << 9, + + /// + /// The server supports the UTF8 extension, + /// allowing clients to retrieve messages in the UTF-8 encoding. + /// + UTF8 = 1 << 10, + + /// + /// The server supports the UTF8=USER extension, + /// allowing clients to authenticate using UTF-8 encoded usernames and passwords. + /// + UTF8User = 1 << 11, + + /// + /// The server supports the LANG extension, + /// allowing clients to specify which language the server should use for error strings. + /// + Lang = 1 << 12, + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Client.cs b/src/MailKit/Net/Pop3/Pop3Client.cs new file mode 100644 index 0000000..cbe1947 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Client.cs @@ -0,0 +1,3401 @@ +// +// Pop3Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using MimeKit; +using MimeKit.IO; + +using MailKit.Security; + +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Pop3 { + /// + /// A POP3 client that can be used to retrieve messages from a server. + /// + /// + /// The class supports both the "pop" and "pops" protocols. The "pop" protocol + /// makes a clear-text connection to the POP3 server and does not use SSL or TLS unless the POP3 server + /// supports the STLS extension. The "pops" protocol, + /// however, connects to the POP3 server using an SSL-wrapped connection. + /// + /// + /// + /// + public partial class Pop3Client : MailSpool, IPop3Client + { + [Flags] + enum ProbedCapabilities : byte { + None = 0, + Top = (1 << 0), + UIDL = (1 << 1) + } + + readonly MimeParser parser = new MimeParser (Stream.Null); + readonly Pop3Engine engine; + ProbedCapabilities probed; + bool disposed, disconnecting, secure, utf8; + int timeout = 2 * 60 * 1000; + long octets; + int total; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can retrieve messages with the , you must first call + /// one of the Connect methods + /// and authenticate using one of the + /// Authenticate methods. + /// + /// + /// + /// + /// The protocol logger. + /// + /// is null. + /// + public Pop3Client (IProtocolLogger protocolLogger) : base (protocolLogger) + { + engine = new Pop3Engine (); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can retrieve messages with the , you must first call + /// one of the Connect methods + /// and authenticate using one of the + /// Authenticate methods. + /// + public Pop3Client () : this (new NullProtocolLogger ()) + { + } + + /// + /// Gets an object that can be used to synchronize access to the POP3 server. + /// + /// + /// Gets an object that can be used to synchronize access to the POP3 server. + /// When using the non-Async methods from multiple threads, it is important to lock the + /// object for thread safety when using the synchronous methods. + /// + /// The lock object. + public override object SyncRoot { + get { return engine; } + } + + /// + /// Gets the protocol supported by the message service. + /// + /// + /// Gets the protocol supported by the message service. + /// + /// The protocol. + protected override string Protocol { + get { return "pop"; } + } + + /// + /// Gets the capabilities supported by the POP3 server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + public Pop3Capabilities Capabilities { + get { return engine.Capabilities; } + set { + if ((engine.Capabilities | value) > engine.Capabilities) + throw new ArgumentException ("Capabilities cannot be enabled, they may only be disabled.", nameof (value)); + + engine.Capabilities = value; + } + } + + /// + /// Gets the expiration policy. + /// + /// + /// If the server supports the EXPIRE capability (), the value + /// of the property will reflect the value advertized by the server. + /// A value of -1 indicates that messages will never expire. + /// A value of 0 indicates that messages that have been retrieved during the current session + /// will be purged immediately after the connection is closed via the QUIT command. + /// Values larger than 0 indicate the minimum number of days that the server will retain + /// messages which have been retrieved. + /// + /// + /// + /// + /// The expiration policy. + public int ExpirePolicy { + get { return engine.ExpirePolicy; } + } + + /// + /// Gets the implementation details of the server. + /// + /// + /// If the server advertizes its implementation details, this value will be set to a string containing the + /// information details provided by the server. + /// + /// The implementation details. + public string Implementation { + get { return engine.Implementation; } + } + + /// + /// Gets the minimum delay, in milliseconds, between logins. + /// + /// + /// If the server supports the LOGIN-DELAY capability (), this value + /// will be set to the minimum number of milliseconds that the client must wait between logins. + /// + /// + /// + /// + /// The login delay. + public int LoginDelay { + get { return engine.LoginDelay; } + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (Pop3Client)); + } + + void CheckConnected () + { + if (!IsConnected) + throw new ServiceNotConnectedException ("The Pop3Client is not connected."); + } + + void CheckAuthenticated () + { + if (!IsAuthenticated) + throw new ServiceNotAuthenticatedException ("The Pop3Client has not been authenticated."); + } + + bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (ServerCertificateValidationCallback != null) + return ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (ServicePointManager.ServerCertificateValidationCallback != null) + return ServicePointManager.ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); +#endif + + return DefaultServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); + } + + static Exception CreatePop3Exception (Pop3Command pc) + { + var command = pc.Command.Split (' ')[0].TrimEnd (); + var message = string.Format ("POP3 server did not respond with a +OK response to the {0} command.", command); + + if (pc.Status == Pop3CommandStatus.Error) + return new Pop3CommandException (message, pc.StatusText); + + return new Pop3ProtocolException (message); + } + + static ProtocolException CreatePop3ParseException (Exception innerException, string format, params object[] args) + { + return new Pop3ProtocolException (string.Format (CultureInfo.InvariantCulture, format, args), innerException); + } + + static ProtocolException CreatePop3ParseException (string format, params object[] args) + { + return new Pop3ProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)); + } + + async Task SendCommandAsync (bool doAsync, CancellationToken token, string command) + { + var pc = engine.QueueCommand (token, null, Encoding.ASCII, command); + int id; + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + } + + Task SendCommandAsync (bool doAsync, CancellationToken token, string format, params object[] args) + { + return SendCommandAsync (doAsync, token, Encoding.ASCII, format, args); + } + + async Task SendCommandAsync (bool doAsync, CancellationToken token, Encoding encoding, string format, params object[] args) + { + string okText = string.Empty; + int id; + + var pc = engine.QueueCommand (token, (pop3, cmd, text, xdoAsync) => { + if (cmd.Status == Pop3CommandStatus.Ok) + okText = text; + + return Task.FromResult (true); + }, encoding, format, args); + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + return okText; + } + + #region IMailService implementation + + /// + /// Gets the authentication mechanisms supported by the POP3 server. + /// + /// + /// The authentication mechanisms are queried as part of the + /// connection process. + /// Servers that do not support the SASL capability will typically + /// support either the APOP authentication mechanism + /// () or the ability to login using the + /// USER and PASS commands (). + /// + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before authenticating. + /// In the case of the APOP authentication mechanism, remove it from the + /// property instead. + /// + /// + /// + /// + /// The authentication mechanisms. + public override HashSet AuthenticationMechanisms { + get { return engine.AuthenticationMechanisms; } + } + + /// + /// Gets or sets the timeout for network streaming operations, in milliseconds. + /// + /// + /// Gets or sets the underlying socket stream's + /// and values. + /// + /// The timeout in milliseconds. + public override int Timeout { + get { return timeout; } + set { + if (IsConnected && engine.Stream.CanTimeout) { + engine.Stream.WriteTimeout = value; + engine.Stream.ReadTimeout = value; + } + + timeout = value; + } + } + + /// + /// Gets whether or not the client is currently connected to an POP3 server. + /// + /// + /// The state is set to true immediately after + /// one of the Connect + /// methods succeeds and is not set back to false until either the client + /// is disconnected via or until a + /// is thrown while attempting to read or write to + /// the underlying network socket. + /// When an is caught, the connection state of the + /// should be checked before continuing. + /// + /// + /// + /// + /// true if the client is connected; otherwise, false. + public override bool IsConnected { + get { return engine.IsConnected; } + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// true if the connection is secure; otherwise, false. + public override bool IsSecure { + get { return IsConnected && secure; } + } + + /// + /// Get whether or not the client is currently authenticated with the POP3 server. + /// + /// + /// Gets whether or not the client is currently authenticated with the POP3 server. + /// To authenticate with the POP3 server, use one of the + /// Authenticate methods. + /// + /// true if the client is connected; otherwise, false. + public override bool IsAuthenticated { + get { return engine.State == Pop3EngineState.Transaction; } + } + + async Task UpdateMessageCountAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = engine.QueueCommand (cancellationToken, (pop3, cmd, text, xdoAsync) => { + if (cmd.Status != Pop3CommandStatus.Ok) + return Task.FromResult (false); + + // the response should be " " + var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (tokens.Length < 2) { + cmd.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the STAT command: {0}", text); + return Task.FromResult (false); + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out total) || total < 0) { + cmd.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the STAT command: {0}", text); + return Task.FromResult (false); + } + + if (!long.TryParse (tokens[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out octets)) { + cmd.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the STAT command: {0}", text); + return Task.FromResult (false); + } + + return Task.FromResult (true); + }, "STAT"); + int id; + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + } + + async Task ProbeCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken) + { + if ((engine.Capabilities & Pop3Capabilities.UIDL) == 0 && (probed & ProbedCapabilities.UIDL) == 0) { + // if the message count is > 0, we can probe the UIDL command + if (total > 0) { + try { + var ctx = new MessageUidContext (this, 1); + + await ctx.GetUidAsync (doAsync, cancellationToken).ConfigureAwait (false); + } catch (NotSupportedException) { + } + } + } + } + + async Task QueryCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken) + { + if (doAsync) + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + else + engine.QueryCapabilities (cancellationToken); + } + + class SaslAuthContext + { + readonly SaslMechanism mechanism; + readonly Pop3Client client; + + public SaslAuthContext (Pop3Client client, SaslMechanism mechanism) + { + this.mechanism = mechanism; + this.client = client; + } + + public string AuthMessage { + get; private set; + } + + Pop3Engine Engine { + get { return client.engine; } + } + + async Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + while (pc.Status == Pop3CommandStatus.Continue && !mechanism.IsAuthenticated) { + var challenge = mechanism.Challenge (text); + + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + string response; + + if (doAsync) { + await pop3.Stream.WriteAsync (buf, 0, buf.Length, pc.CancellationToken).ConfigureAwait (false); + await pop3.Stream.FlushAsync (pc.CancellationToken).ConfigureAwait (false); + + response = (await pop3.ReadLineAsync (pc.CancellationToken).ConfigureAwait (false)).TrimEnd (); + } else { + pop3.Stream.Write (buf, 0, buf.Length, pc.CancellationToken); + pop3.Stream.Flush (pc.CancellationToken); + + response = pop3.ReadLine (pc.CancellationToken).TrimEnd (); + } + + pc.Status = Pop3Engine.GetCommandStatus (response, out text); + pc.StatusText = text; + + if (pc.Status == Pop3CommandStatus.ProtocolError) + throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + } + + AuthMessage = text; + } + + public async Task AuthenticateAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (cancellationToken, OnDataReceived, "AUTH {0}", mechanism.MechanismName); + int id; + + AuthMessage = string.Empty; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + return pc; + } + } + + async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, CancellationToken cancellationToken) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The Pop3Client must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The Pop3Client is already authenticated."); + + CheckDisposed (); + + cancellationToken.ThrowIfCancellationRequested (); + + mechanism.Uri = new Uri ("pop://" + engine.Uri.Host); + + var ctx = new SaslAuthContext (this, mechanism); + var pc = await ctx.AuthenticateAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (pc.Status == Pop3CommandStatus.Error) + throw new AuthenticationException (); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + + engine.State = Pop3EngineState.Transaction; + + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + await UpdateMessageCountAsync (doAsync, cancellationToken).ConfigureAwait (false); + await ProbeCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (ctx.AuthMessage); + } + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (mechanism, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool doAsync, CancellationToken cancellationToken) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The Pop3Client must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The Pop3Client is already authenticated."); + + CheckDisposed (); + + var saslUri = new Uri ("pop://" + engine.Uri.Host); + string userName, password, message = null; + NetworkCredential cred; + + if ((engine.Capabilities & Pop3Capabilities.Apop) != 0) { + cred = credentials.GetCredential (saslUri, "APOP"); + userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; + password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + var challenge = engine.ApopToken + password; + var md5sum = new StringBuilder (); + byte[] digest; + + using (var md5 = MD5.Create ()) + digest = md5.ComputeHash (encoding.GetBytes (challenge)); + + for (int i = 0; i < digest.Length; i++) + md5sum.Append (digest[i].ToString ("x2")); + + try { + message = await SendCommandAsync (doAsync, cancellationToken, encoding, "APOP {0} {1}", userName, md5sum).ConfigureAwait (false); + engine.State = Pop3EngineState.Transaction; + } catch (Pop3CommandException) { + } + + if (engine.State == Pop3EngineState.Transaction) { + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + await UpdateMessageCountAsync (doAsync, cancellationToken).ConfigureAwait (false); + await ProbeCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (message ?? string.Empty); + return; + } + } + + if ((engine.Capabilities & Pop3Capabilities.Sasl) != 0) { + foreach (var authmech in SaslMechanism.AuthMechanismRank) { + SaslMechanism sasl; + + if (!engine.AuthenticationMechanisms.Contains (authmech)) + continue; + + if ((sasl = SaslMechanism.Create (authmech, saslUri, encoding, credentials)) == null) + continue; + + cancellationToken.ThrowIfCancellationRequested (); + + var ctx = new SaslAuthContext (this, sasl); + var pc = await ctx.AuthenticateAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (pc.Status == Pop3CommandStatus.Error) + continue; + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + + engine.State = Pop3EngineState.Transaction; + + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + await UpdateMessageCountAsync (doAsync, cancellationToken).ConfigureAwait (false); + await ProbeCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (ctx.AuthMessage); + return; + } + } + + // fall back to the classic USER & PASS commands... + cred = credentials.GetCredential (saslUri, "DEFAULT"); + userName = utf8 ? SaslMechanism.SaslPrep (cred.UserName) : cred.UserName; + password = utf8 ? SaslMechanism.SaslPrep (cred.Password) : cred.Password; + + try { + await SendCommandAsync (doAsync, cancellationToken, encoding, "USER {0}", userName).ConfigureAwait (false); + message = await SendCommandAsync (doAsync, cancellationToken, encoding, "PASS {0}", password).ConfigureAwait (false); + } catch (Pop3CommandException) { + throw new AuthenticationException (); + } + + engine.State = Pop3EngineState.Transaction; + + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + await UpdateMessageCountAsync (doAsync, cancellationToken).ConfigureAwait (false); + await ProbeCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (message); + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// If the POP3 server supports the APOP authentication mechanism, + /// then APOP is used. + /// If the APOP authentication mechanism is not supported and the + /// server supports one or more SASL authentication mechanisms, then + /// the SASL mechanisms that both the client and server support are tried + /// in order of greatest security to weakest security. Once a SASL + /// authentication mechanism is found that both client and server support, + /// the credentials are used to authenticate. + /// If the server does not support SASL or if no common SASL mechanisms + /// can be found, then the USER and PASS commands are used as a + /// fallback. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// In the case of the APOP authentication mechanism, remove it from the + /// property instead. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// An POP3 protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (encoding, credentials, false, cancellationToken).GetAwaiter ().GetResult (); + } + + internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + CheckDisposed (); + + probed = ProbedCapabilities.None; + secure = false; + + engine.Uri = new Uri ($"pop://{host}:110"); + engine.Connect (new Pop3Stream (replayStream, ProtocolLogger), cancellationToken); + engine.QueryCapabilities (cancellationToken); + engine.Disconnected += OnEngineDisconnected; + OnConnected (host, 110, SecureSocketOptions.None); + } + + internal async Task ReplayConnectAsync (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + CheckDisposed (); + + probed = ProbedCapabilities.None; + secure = false; + + engine.Uri = new Uri ($"pop://{host}:110"); + await engine.ConnectAsync (new Pop3Stream (replayStream, ProtocolLogger), cancellationToken).ConfigureAwait (false); + await engine.QueryCapabilitiesAsync (cancellationToken).ConfigureAwait (false); + engine.Disconnected += OnEngineDisconnected; + OnConnected (host, 110, SecureSocketOptions.None); + } + + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + { + switch (options) { + default: + if (port == 0) + port = 110; + break; + case SecureSocketOptions.Auto: + switch (port) { + case 0: port = 110; goto default; + case 995: options = SecureSocketOptions.SslOnConnect; break; + default: options = SecureSocketOptions.StartTlsWhenAvailable; break; + } + break; + case SecureSocketOptions.SslOnConnect: + if (port == 0) + port = 995; + break; + } + + switch (options) { + case SecureSocketOptions.StartTlsWhenAvailable: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}/?starttls=when-available", host, port)); + starttls = true; + break; + case SecureSocketOptions.StartTls: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}/?starttls=always", host, port)); + starttls = true; + break; + case SecureSocketOptions.SslOnConnect: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pops://{0}:{1}", host, port)); + starttls = false; + break; + default: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "pop://{0}:{1}", host, port)); + starttls = false; + break; + } + } + + async Task ConnectAsync (string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The Pop3Client is already connected."); + + Stream stream; + bool starttls; + Uri uri; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + var socket = await ConnectSocket (host, port, doAsync, cancellationToken).ConfigureAwait (false); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + secure = true; + stream = ssl; + } else { + stream = new NetworkStream (socket, true); + secure = false; + } + + probed = ProbedCapabilities.None; + if (stream.CanTimeout) { + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + } + + try { + ProtocolLogger.LogConnect (uri); + } catch { + stream.Dispose (); + secure = false; + throw; + } + + var pop3 = new Pop3Stream (stream, ProtocolLogger); + + if (doAsync) + await engine.ConnectAsync (pop3, cancellationToken).ConfigureAwait (false); + else + engine.Connect (pop3, cancellationToken); + + try { + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & Pop3Capabilities.StartTLS) == 0) + throw new NotSupportedException ("The POP3 server does not support the STLS extension."); + + if (starttls && (engine.Capabilities & Pop3Capabilities.StartTLS) != 0) { + await SendCommandAsync (doAsync, cancellationToken, "STLS").ConfigureAwait (false); + + try { + var tls = new SslStream (stream, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // re-issue a CAPA command + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + } catch { + engine.Disconnect (); + secure = false; + throw; + } + + engine.Disconnected += OnEngineDisconnected; + OnConnected (host, port, options); + } + + /// + /// Establish a connection to the specified POP3 or POP3/S server. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 995. All other values will use a default port of 110. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// + /// + /// + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The Pop3Client is already connected."); + + Stream network; + bool starttls; + Uri uri; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + network = ssl; + secure = true; + } else { + network = stream; + secure = false; + } + + probed = ProbedCapabilities.None; + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + try { + ProtocolLogger.LogConnect (uri); + } catch { + network.Dispose (); + secure = false; + throw; + } + + var pop3 = new Pop3Stream (network, ProtocolLogger); + + if (doAsync) + await engine.ConnectAsync (pop3, cancellationToken).ConfigureAwait (false); + else + engine.Connect (pop3, cancellationToken); + + try { + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (engine.Capabilities & Pop3Capabilities.StartTLS) == 0) + throw new NotSupportedException ("The POP3 server does not support the STLS extension."); + + if (starttls && (engine.Capabilities & Pop3Capabilities.StartTLS) != 0) { + await SendCommandAsync (doAsync, cancellationToken, "STLS").ConfigureAwait (false); + + var tls = new SslStream (network, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + try { + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // re-issue a CAPA command + await QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + } catch { + engine.Disconnect (); + secure = false; + throw; + } + + engine.Disconnected += OnEngineDisconnected; + OnConnected (host, port, options); + } + + Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); + + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, doAsync, cancellationToken); + } + + /// + /// Establish a connection to the specified POP3 or POP3/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (socket, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Establish a connection to the specified POP3 or POP3/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified POP3 or POP3/S server using + /// the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 995, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the POP3 server does not support the STLS extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (stream, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DisconnectAsync (bool quit, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (!engine.IsConnected) + return; + + if (quit) { + try { + await SendCommandAsync (doAsync, cancellationToken, "QUIT").ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (Pop3ProtocolException) { + } catch (Pop3CommandException) { + } catch (IOException) { + } + } + + disconnecting = true; + engine.Disconnect (); + } + + /// + /// Disconnect the service. + /// + /// + /// If is true, a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// If set to true, a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + DisconnectAsync (quit, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the message count. + /// + /// + /// Gets the message count. + /// + /// The message count. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override int GetMessageCount (CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + UpdateMessageCountAsync (false, cancellationToken).GetAwaiter ().GetResult (); + + return Count; + } + + Task NoOpAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return SendCommandAsync (doAsync, cancellationToken, "NOOP"); + } + + /// + /// Ping the POP3 server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) + { + NoOpAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + void OnEngineDisconnected (object sender, EventArgs e) + { + var options = SecureSocketOptions.None; + bool requested = disconnecting; + string host = null; + int port = 0; + + if (engine.Uri != null) { + options = GetSecureSocketOptions (engine.Uri); + host = engine.Uri.Host; + port = engine.Uri.Port; + } + + engine.Disconnected -= OnEngineDisconnected; + disconnecting = secure = utf8 = false; + octets = total = 0; + engine.Uri = null; + + if (host != null) + OnDisconnected (host, port, options, requested); + } + + #endregion + + async Task EnableUTF8Async (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + + if (engine.State != Pop3EngineState.Connected) + throw new InvalidOperationException ("You must enable UTF-8 mode before authenticating."); + + if ((engine.Capabilities & Pop3Capabilities.UTF8) == 0) + throw new NotSupportedException ("The POP3 server does not support the UTF8 extension."); + + if (utf8) + return; + + await SendCommandAsync (doAsync, cancellationToken, "UTF8").ConfigureAwait (false); + utf8 = true; + } + + /// + /// Enable UTF8 mode. + /// + /// + /// The POP3 UTF8 extension allows the client to retrieve messages in the UTF-8 encoding and + /// may also allow the user to authenticate using a UTF-8 encoded username or password. + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The has already been authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the UTF8 extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)) + { + EnableUTF8Async (false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task> GetLanguagesAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + + if ((Capabilities & Pop3Capabilities.Lang) == 0) + throw new NotSupportedException ("The POP3 server does not support the LANG extension."); + + var langs = new List (); + + var pc = engine.QueueCommand (cancellationToken, async (pop3, cmd, text, xdoAsync) => { + if (cmd.Status != Pop3CommandStatus.Ok) + return; + + do { + string response; + + if (xdoAsync) + response = await engine.ReadLineAsync (cmd.CancellationToken).ConfigureAwait (false); + else + response = engine.ReadLine (cmd.CancellationToken); + + if (response == ".") + break; + + var tokens = response.Split (new [] { ' ' }, 2); + if (tokens.Length != 2) + continue; + + langs.Add (new Pop3Language (tokens[0], tokens[1])); + } while (true); + }, "LANG"); + int id; + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + + return new ReadOnlyCollection (langs); + } + + /// + /// Get the list of languages supported by the POP3 server. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// query the list of languages supported by the POP3 server that can + /// be used for error messages. + /// + /// The supported languages. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public IList GetLanguages (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetLanguagesAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + Task SetLanguageAsync (string lang, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + + if (lang == null) + throw new ArgumentNullException (nameof (lang)); + + if (lang.Length == 0) + throw new ArgumentException ("The language code cannot be empty.", nameof (lang)); + + if ((Capabilities & Pop3Capabilities.Lang) == 0) + throw new NotSupportedException ("The POP3 server does not support the LANG extension."); + + return SendCommandAsync (doAsync, cancellationToken, "LANG {0}", lang); + } + + /// + /// Set the language used by the POP3 server for error messages. + /// + /// + /// If the POP3 server supports the LANG extension, it is possible to + /// set the language used by the POP3 server for error messages. + /// + /// The language code. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The POP3 server does not support the LANG extension. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public void SetLanguage (string lang, CancellationToken cancellationToken = default (CancellationToken)) + { + SetLanguageAsync (lang, false, cancellationToken).GetAwaiter ().GetResult (); + } + + #region IMailSpool implementation + + /// + /// Get the number of messages available in the message spool. + /// + /// + /// Gets the number of messages available on the POP3 server. + /// Once authenticated, the property will be set + /// to the number of available messages on the POP3 server. + /// + /// + /// + /// + /// The message count. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public override int Count { + get { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return total; + } + } + + /// + /// Gets whether or not the supports referencing messages by UIDs. + /// + /// + /// Not all servers support referencing messages by UID, so this property should + /// be checked before using + /// and . + /// If the server does not support UIDs, then all methods that take UID arguments + /// along with and + /// will fail. + /// + /// true if supports UIDs; otherwise, false. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + public override bool SupportsUids { + get { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return (engine.Capabilities & Pop3Capabilities.UIDL) != 0; + } + } + + class MessageUidContext + { + readonly Pop3Client client; + readonly int seqid; + string uid; + + public MessageUidContext (Pop3Client client, int seqid) + { + this.client = client; + this.seqid = seqid; + } + + Pop3Engine Engine { + get { return client.engine; } + } + + Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.FromResult (true); + + var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + int id; + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); + return Task.FromResult (true); + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out id) || id != seqid) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected response to the UIDL command."); + return Task.FromResult (true); + } + + uid = tokens[1]; + + return Task.FromResult (true); + } + + public async Task GetUidAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (cancellationToken, OnDataReceived, "UIDL {0}", seqid.ToString (CultureInfo.InvariantCulture)); + int id; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + client.probed |= ProbedCapabilities.UIDL; + + if (pc.Status != Pop3CommandStatus.Ok) { + if (!client.SupportsUids) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); + + throw CreatePop3Exception (pc); + } + + if (pc.Exception != null) + throw pc.Exception; + + Engine.Capabilities |= Pop3Capabilities.UIDL; + + return uid; + } + } + + Task GetMessageUidAsync (int index, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (!SupportsUids && (probed & ProbedCapabilities.UIDL) != 0) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); + + var ctx = new MessageUidContext (this, index + 1); + + return ctx.GetUidAsync (doAsync, cancellationToken); + } + + /// + /// Get the UID of the message at the specified index. + /// + /// + /// Gets the UID of the message at the specified index. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// The message UID. + /// The message index. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override string GetMessageUid (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageUidAsync (index, false, cancellationToken).GetAwaiter ().GetResult (); + } + + class MessageUidsContext + { + readonly Pop3Client client; + readonly List uids; + + public MessageUidsContext (Pop3Client client) + { + uids = new List (); + this.client = client; + } + + Pop3Engine Engine { + get { return client.engine; } + } + + async Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return; + + do { + string response; + + if (doAsync) + response = await Engine.ReadLineAsync (pc.CancellationToken).ConfigureAwait (false); + else + response = Engine.ReadLine (pc.CancellationToken); + + if (response == ".") + break; + + if (pc.Exception != null) + continue; + + var tokens = response.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + int seqid; + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the UIDL command."); + continue; + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out seqid) || seqid != uids.Count + 1) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an invalid response to the UIDL command."); + continue; + } + + uids.Add (tokens[1]); + } while (true); + } + + public async Task> GetUidsAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (cancellationToken, OnDataReceived, "UIDL"); + int id; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + client.probed |= ProbedCapabilities.UIDL; + + if (pc.Status != Pop3CommandStatus.Ok) { + if (!client.SupportsUids) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); + + throw CreatePop3Exception (pc); + } + + if (pc.Exception != null) + throw pc.Exception; + + Engine.Capabilities |= Pop3Capabilities.UIDL; + + return uids; + } + } + + Task> GetMessageUidsAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (!SupportsUids && (probed & ProbedCapabilities.UIDL) != 0) + throw new NotSupportedException ("The POP3 server does not support the UIDL extension."); + + var ctx = new MessageUidsContext (this); + + return ctx.GetUidsAsync (doAsync, cancellationToken); + } + + /// + /// Get the full list of available message UIDs. + /// + /// + /// Gets the full list of available message UIDs. + /// Not all servers support UIDs, so you should first check the + /// property for the flag or + /// the convenience property. + /// + /// + /// + /// + /// The message uids. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessageUids (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageUidsAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + class MessageSizeContext + { + readonly Pop3Client client; + readonly int seqid; + int size; + + public MessageSizeContext (Pop3Client client, int seqid) + { + this.client = client; + this.seqid = seqid; + } + + Pop3Engine Engine { + get { return client.engine; } + } + + Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return Task.FromResult (true); + + var tokens = text.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + int id; + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command: {0}", text); + return Task.FromResult (true); + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out id) || id != seqid) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected sequence-id token to the LIST command: {0}", tokens[0]); + return Task.FromResult (true); + } + + if (!int.TryParse (tokens[1], NumberStyles.None, CultureInfo.InvariantCulture, out size) || size < 0) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command: {0}", tokens[1]); + return Task.FromResult (true); + } + + return Task.FromResult (true); + } + + public async Task GetSizeAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (cancellationToken, OnDataReceived, "LIST {0}", seqid.ToString (CultureInfo.InvariantCulture)); + int id; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + + return size; + } + } + + Task GetMessageSizeAsync (int index, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new MessageSizeContext (this, index + 1); + + return ctx.GetSizeAsync (doAsync, cancellationToken); + } + + /// + /// Get the size of the specified message, in bytes. + /// + /// + /// Gets the size of the specified message, in bytes. + /// + /// The message size, in bytes. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override int GetMessageSize (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageSizeAsync (index, false, cancellationToken).GetAwaiter ().GetResult (); + } + + class MessageSizesContext + { + readonly Pop3Client client; + readonly List sizes; + + public MessageSizesContext (Pop3Client client) + { + sizes = new List (); + this.client = client; + } + + Pop3Engine Engine { + get { return client.engine; } + } + + async Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return; + + do { + string response; + + if (doAsync) + response = await Engine.ReadLineAsync (pc.CancellationToken).ConfigureAwait (false); + else + response = Engine.ReadLine (pc.CancellationToken); + + if (response == ".") + break; + + if (pc.Exception != null) + continue; + + var tokens = response.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + int seqid, size; + + if (tokens.Length < 2) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an incomplete response to the LIST command: {0}", response); + continue; + } + + if (!int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out seqid) || seqid != sizes.Count + 1) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected sequence-id token to the LIST command: {0}", tokens[0]); + continue; + } + + if (!int.TryParse (tokens[1], NumberStyles.None, CultureInfo.InvariantCulture, out size) || size < 0) { + pc.Exception = CreatePop3ParseException ("Pop3 server returned an unexpected size token to the LIST command: {0}", tokens[1]); + continue; + } + + sizes.Add (size); + } while (true); + } + + public async Task> GetSizesAsync (bool doAsync, CancellationToken cancellationToken) + { + var pc = Engine.QueueCommand (cancellationToken, OnDataReceived, "LIST"); + int id; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + + return sizes; + } + } + + Task> GetMessageSizesAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + var ctx = new MessageSizesContext (this); + + return ctx.GetSizesAsync (doAsync, cancellationToken); + } + + /// + /// Get the sizes for all available messages, in bytes. + /// + /// + /// Gets the sizes for all available messages, in bytes. + /// + /// The message sizes, in bytes. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessageSizes (CancellationToken cancellationToken = default (CancellationToken)) + { + return GetMessageSizesAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + abstract class DownloadContext + { + readonly ITransferProgress progress; + readonly Pop3Client client; + T[] downloaded; + long nread; + int index; + + protected DownloadContext (Pop3Client client, ITransferProgress progress) + { + this.progress = progress; + this.client = client; + } + + protected Pop3Engine Engine { + get { return client.engine; } + } + + protected abstract T Parse (Pop3Stream data, CancellationToken cancellationToken); + + protected abstract Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken); + + protected void Update (int n) + { + if (progress == null) + return; + + nread += n; + + progress.Report (nread); + } + + async Task OnDataReceived (Pop3Engine pop3, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return; + + try { + T item; + + pop3.Stream.Mode = Pop3StreamMode.Data; + + if (doAsync) + item = await ParseAsync (pop3.Stream, pc.CancellationToken).ConfigureAwait (false); + else + item = Parse (pop3.Stream, pc.CancellationToken); + + downloaded[index++] = item; + } catch (FormatException ex) { + pc.Exception = CreatePop3ParseException (ex, "Failed to parse data."); + + if (doAsync) + await pop3.Stream.CopyToAsync (Stream.Null, 4096, pc.CancellationToken).ConfigureAwait (false); + else + pop3.Stream.CopyTo (Stream.Null, 4096); + } finally { + pop3.Stream.Mode = Pop3StreamMode.Line; + } + } + + Pop3Command QueueCommand (int seqid, bool headersOnly, CancellationToken cancellationToken) + { + if (headersOnly) + return Engine.QueueCommand (cancellationToken, OnDataReceived, "TOP {0} 0", seqid.ToString (CultureInfo.InvariantCulture)); + + return Engine.QueueCommand (cancellationToken, OnDataReceived, "RETR {0}", seqid.ToString (CultureInfo.InvariantCulture)); + } + + async Task DownloadItemAsync (int seqid, bool headersOnly, bool doAsync, CancellationToken cancellationToken) + { + var pc = QueueCommand (seqid, headersOnly, cancellationToken); + int id; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + if (pc.Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (pc); + + if (pc.Exception != null) + throw pc.Exception; + } + + public async Task DownloadAsync (int seqid, bool headersOnly, bool doAsync, CancellationToken cancellationToken) + { + downloaded = new T[1]; + index = 0; + + await DownloadItemAsync (seqid, headersOnly, doAsync, cancellationToken).ConfigureAwait (false); + + return downloaded[0]; + } + + public async Task> DownloadAsync (IList seqids, bool headersOnly, bool doAsync, CancellationToken cancellationToken) + { + downloaded = new T[seqids.Count]; + index = 0; + + if ((Engine.Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < seqids.Count; i++) + await DownloadItemAsync (seqids[i], headersOnly, doAsync, cancellationToken); + + return downloaded; + } + + var commands = new Pop3Command[seqids.Count]; + Pop3Command pc = null; + int id; + + for (int i = 0; i < seqids.Count; i++) + commands[i] = QueueCommand (seqids[i], headersOnly, cancellationToken); + + pc = commands[commands.Length - 1]; + + do { + if (doAsync) + id = await Engine.IterateAsync ().ConfigureAwait (false); + else + id = Engine.Iterate (); + } while (id < pc.Id); + + for (int i = 0; i < commands.Length; i++) { + if (commands[i].Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (commands[i]); + + if (commands[i].Exception != null) + throw commands[i].Exception; + } + + return downloaded; + } + } + + class DownloadStreamContext : DownloadContext + { + public DownloadStreamContext (Pop3Client client, ITransferProgress progress = null) : base (client, progress) + { + } + + protected override Stream Parse (Pop3Stream data, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var stream = new MemoryBlockStream (); + var buffer = new byte[4096]; + int nread; + + while ((nread = data.Read (buffer, 0, buffer.Length, cancellationToken)) > 0) { + stream.Write (buffer, 0, nread); + Update (nread); + } + + stream.Position = 0; + + return stream; + } + + protected override async Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + var stream = new MemoryBlockStream (); + var buffer = new byte[4096]; + int nread; + + while ((nread = await data.ReadAsync (buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false)) > 0) { + stream.Write (buffer, 0, nread); + Update (nread); + } + + stream.Position = 0; + + return stream; + } + } + + class DownloadHeaderContext : DownloadContext + { + readonly MimeParser parser; + + public DownloadHeaderContext (Pop3Client client, MimeParser parser) : base (client, null) + { + this.parser = parser; + } + + protected override HeaderList Parse (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (ParserOptions.Default, stream); + + return parser.ParseMessage (cancellationToken).Headers; + } + } + + protected override async Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (ParserOptions.Default, stream); + + return (await parser.ParseMessageAsync (cancellationToken).ConfigureAwait (false)).Headers; + } + } + } + + class DownloadMessageContext : DownloadContext + { + readonly MimeParser parser; + + public DownloadMessageContext (Pop3Client client, MimeParser parser, ITransferProgress progress = null) : base (client, progress) + { + this.parser = parser; + } + + protected override MimeMessage Parse (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (ParserOptions.Default, stream); + + return parser.ParseMessage (cancellationToken); + } + } + + protected override Task ParseAsync (Pop3Stream data, CancellationToken cancellationToken) + { + using (var stream = new ProgressStream (data, Update)) { + parser.SetStream (ParserOptions.Default, stream); + + return parser.ParseMessageAsync (cancellationToken); + } + } + } + + /// + /// Get the headers for the message at the specified index. + /// + /// + /// Gets the headers for the message at the specified index. + /// + /// The message headers. + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override HeaderList GetMessageHeaders (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (index + 1, true, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the headers for the messages at the specified indexes. + /// + /// + /// Gets the headers for the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers for the specified messages. + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessageHeaders (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return new HeaderList[0]; + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (seqids, true, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the headers of the messages within the specified range. + /// + /// + /// Gets the headers of the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message because + /// it will batch the commands to reduce latency. + /// + /// The headers of the messages within the specified range. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessageHeaders (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return new HeaderList[0]; + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadHeaderContext (this, parser); + + return ctx.DownloadAsync (seqids, true, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the message at the specified index. + /// + /// + /// Gets the message at the specified index. + /// + /// + /// + /// + /// The message. + /// The index of the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override MimeMessage GetMessage (int index, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (index + 1, false, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the messages at the specified indexes. + /// + /// + /// Gets the messages at the specified indexes. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The messages. + /// The indexes of the messages. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return new MimeMessage[0]; + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (seqids, false, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the messages within the specified range. + /// + /// + /// Gets the messages within the specified range. + /// When the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// + /// + /// + /// The messages. + /// The index of the first message to get. + /// The number of messages to get. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return new MimeMessage[0]; + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadMessageContext (this, parser, progress); + + return ctx.DownloadAsync (seqids, false, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the message or header stream at the specified index. + /// + /// + /// Gets the message or header stream at the specified index. + /// + /// The message or header stream. + /// The index of the message. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override Stream GetStream (int index, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (index + 1, headersOnly, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the message or header streams at the specified indexes. + /// + /// + /// Get the message or header streams at the specified indexes. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The indexes of the messages. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetStreams (IList indexes, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return new Stream[0]; + + var seqids = new int[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = indexes[i] + 1; + } + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (seqids, headersOnly, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Get the message or header streams within the specified range. + /// + /// + /// Gets the message or header streams within the specified range. + /// If the POP3 server supports the + /// extension, this method will likely be more efficient than using + /// for each message + /// because it will batch the commands to reduce latency. + /// + /// The message or header streams. + /// The index of the first stream to get. + /// The number of streams to get. + /// true if only the headers should be retrieved; otherwise, false. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The POP3 server does not support the UIDL extension. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IList GetStreams (int startIndex, int count, bool headersOnly = false, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return new Stream[0]; + + var seqids = new int[count]; + + for (int i = 0; i < count; i++) + seqids[i] = startIndex + i + 1; + + var ctx = new DownloadStreamContext (this, progress); + + return ctx.DownloadAsync (seqids, headersOnly, false, cancellationToken).GetAwaiter ().GetResult (); + } + + Task DeleteMessageAsync (int index, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (index < 0 || index >= total) + throw new ArgumentOutOfRangeException (nameof (index)); + + var seqid = (index + 1).ToString (CultureInfo.InvariantCulture); + + return SendCommandAsync (doAsync, cancellationToken, "DELE {0}", seqid); + } + + /// + /// Mark the specified message for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// The index of the message. + /// The cancellation token. + /// + /// is not a valid message index. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void DeleteMessage (int index, CancellationToken cancellationToken = default (CancellationToken)) + { + DeleteMessageAsync (index, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DeleteMessagesAsync (IList indexes, bool doAsync, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (indexes == null) + throw new ArgumentNullException (nameof (indexes)); + + if (indexes.Count == 0) + return; + + var seqids = new string[indexes.Count]; + + for (int i = 0; i < indexes.Count; i++) { + if (indexes[i] < 0 || indexes[i] >= total) + throw new ArgumentException ("One or more of the indexes are invalid.", nameof (indexes)); + + seqids[i] = (indexes[i] + 1).ToString (CultureInfo.InvariantCulture); + } + + if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < seqids.Length; i++) + await SendCommandAsync (doAsync, cancellationToken, "DELE {0}", seqids[i]).ConfigureAwait (false); + + return; + } + + var commands = new Pop3Command[seqids.Length]; + Pop3Command pc = null; + int id; + + for (int i = 0; i < seqids.Length; i++) { + pc = engine.QueueCommand (cancellationToken, null, "DELE {0}", seqids[i]); + commands[i] = pc; + } + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + for (int i = 0; i < commands.Length; i++) { + if (commands[i].Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (commands[i]); + } + } + + /// + /// Mark the specified messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The indexes of the messages. + /// The cancellation token. + /// + /// is null. + /// + /// + /// One or more of the are invalid. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void DeleteMessages (IList indexes, CancellationToken cancellationToken = default (CancellationToken)) + { + DeleteMessagesAsync (indexes, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DeleteMessagesAsync (int startIndex, int count, bool doAsync, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + if (startIndex < 0 || startIndex >= total) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (count < 0 || count > (total - startIndex)) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (count == 0) + return; + + if ((Capabilities & Pop3Capabilities.Pipelining) == 0) { + for (int i = 0; i < count; i++) { + var seqid = (startIndex + i + 1).ToString (CultureInfo.InvariantCulture); + await SendCommandAsync (doAsync, cancellationToken, "DELE {0}", seqid).ConfigureAwait (false); + } + + return; + } + + var commands = new Pop3Command[count]; + Pop3Command pc = null; + int id; + + for (int i = 0; i < count; i++) { + var seqid = (startIndex + i + 1).ToString (CultureInfo.InvariantCulture); + pc = engine.QueueCommand (cancellationToken, null, "DELE {0}", seqid); + commands[i] = pc; + } + + do { + if (doAsync) + id = await engine.IterateAsync ().ConfigureAwait (false); + else + id = engine.Iterate (); + } while (id < pc.Id); + + for (int i = 0; i < commands.Length; i++) { + if (commands[i].Status != Pop3CommandStatus.Ok) + throw CreatePop3Exception (commands[i]); + } + } + + /// + /// Mark the specified range of messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// + /// + /// + /// The index of the first message to mark for deletion. + /// The number of messages to mark for deletion. + /// The cancellation token. + /// + /// and do not specify + /// a valid range of messages. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void DeleteMessages (int startIndex, int count, CancellationToken cancellationToken = default (CancellationToken)) + { + DeleteMessagesAsync (startIndex, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Mark all messages for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void DeleteAllMessages (CancellationToken cancellationToken = default (CancellationToken)) + { + if (total > 0) + DeleteMessages (0, total, cancellationToken); + } + + Task ResetAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + return SendCommandAsync (doAsync, cancellationToken, "RSET"); + } + + /// + /// Reset the state of all messages marked for deletion. + /// + /// + /// Messages marked for deletion are not actually deleted until the session + /// is cleanly disconnected + /// (see ). + /// + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// The POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override void Reset (CancellationToken cancellationToken = default (CancellationToken)) + { + ResetAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the messages in the folder. + /// + /// + /// Gets an enumerator for the messages in the folder. + /// + /// The enumerator. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The is not authenticated. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// A POP3 command failed. + /// + /// + /// A POP3 protocol error occurred. + /// + public override IEnumerator GetEnumerator () + { + CheckDisposed (); + CheckConnected (); + CheckAuthenticated (); + + for (int i = 0; i < total; i++) + yield return GetMessage (i, CancellationToken.None); + + yield break; + } + + #endregion + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + engine.Disconnect (); + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Command.cs b/src/MailKit/Net/Pop3/Pop3Command.cs new file mode 100644 index 0000000..9b43ec9 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Command.cs @@ -0,0 +1,74 @@ +// +// Pop3Command.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; + +namespace MailKit.Net.Pop3 { + /// + /// POP3 command handler. + /// + /// + /// All exceptions thrown by the handler are considered fatal and will + /// force-disconnect the connection. If a non-fatal error occurs, set + /// it on the property. + /// + delegate Task Pop3CommandHandler (Pop3Engine engine, Pop3Command pc, string text, bool doAsync); + + enum Pop3CommandStatus { + Queued = -5, + Active = -4, + Continue = -3, + ProtocolError = -2, + Error = -1, + Ok = 0 + } + + class Pop3Command + { + public CancellationToken CancellationToken { get; private set; } + public Pop3CommandHandler Handler { get; private set; } + public Encoding Encoding { get; private set; } + public string Command { get; private set; } + public int Id { get; internal set; } + + // output + public Pop3CommandStatus Status { get; internal set; } + public ProtocolException Exception { get; set; } + public string StatusText { get; set; } + + public Pop3Command (CancellationToken cancellationToken, Pop3CommandHandler handler, Encoding encoding, string format, params object[] args) + { + Command = string.Format (CultureInfo.InvariantCulture, format, args); + CancellationToken = cancellationToken; + Encoding = encoding; + Handler = handler; + } + } +} diff --git a/src/MailKit/Net/Pop3/Pop3CommandException.cs b/src/MailKit/Net/Pop3/Pop3CommandException.cs new file mode 100644 index 0000000..eee7b4f --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3CommandException.cs @@ -0,0 +1,179 @@ +// +// Pop3CommandException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Pop3 { + /// + /// A POP3 command exception. + /// + /// + /// The exception that is thrown when a POP3 command fails. Unlike a , + /// a does not require the to be reconnected. + /// + /// + /// + /// +#if SERIALIZABLE + [Serializable] +#endif + public class Pop3CommandException : CommandException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected Pop3CommandException (SerializationInfo info, StreamingContext context) : base (info, context) + { + StatusText = info.GetString ("StatusText"); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public Pop3CommandException (string message, Exception innerException) : base (message, innerException) + { + StatusText = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The response status text. + /// An inner exception. + /// + /// is null. + /// + public Pop3CommandException (string message, string statusText, Exception innerException) : base (message, innerException) + { + if (statusText == null) + throw new ArgumentNullException (nameof (statusText)); + + StatusText = statusText; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public Pop3CommandException (string message) : base (message) + { + StatusText = string.Empty; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The response status text. + /// + /// is null. + /// + public Pop3CommandException (string message, string statusText) : base (message) + { + if (statusText == null) + throw new ArgumentNullException (nameof (statusText)); + + StatusText = statusText; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public Pop3CommandException () + { + StatusText = string.Empty; + } + + /// + /// Get the response status text. + /// + /// + /// Gets the response status text. + /// + /// + /// + /// + /// The response status text. + public string StatusText { + get; private set; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("StatusText", StatusText); + } +#endif + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Engine.cs b/src/MailKit/Net/Pop3/Pop3Engine.cs new file mode 100644 index 0000000..8b3d3b9 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Engine.cs @@ -0,0 +1,655 @@ +// +// Pop3Engine.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MailKit.Net.Pop3 { + /// + /// The state of the . + /// + enum Pop3EngineState { + /// + /// The Pop3Engine is in the disconnected state. + /// + Disconnected, + + /// + /// The Pop3Engine is in the connected state. + /// + Connected, + + /// + /// The Pop3Engine is in the transaction state, indicating that it is + /// authenticated and may retrieve messages from the server. + /// + Transaction + } + + /// + /// A POP3 command engine. + /// + class Pop3Engine + { + static readonly Encoding Latin1; + static readonly Encoding UTF8; + + readonly List queue; + Pop3Stream stream; + int nextId; + + static Pop3Engine () + { + UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); + + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.GetEncoding (1252); + } + } + + /// + /// Initializes a new instance of the class. + /// + public Pop3Engine () + { + AuthenticationMechanisms = new HashSet (StringComparer.Ordinal); + Capabilities = Pop3Capabilities.User; + queue = new List (); + nextId = 1; + } + + /// + /// Gets the URI of the POP3 server. + /// + /// + /// Gets the URI of the POP3 server. + /// + /// The URI of the POP3 server. + public Uri Uri { + get; internal set; + } + + /// + /// Gets the authentication mechanisms supported by the POP3 server. + /// + /// + /// The authentication mechanisms are queried durring the + /// method. + /// + /// The authentication mechanisms. + public HashSet AuthenticationMechanisms { + get; private set; + } + + /// + /// Gets the capabilities supported by the POP3 server. + /// + /// + /// The capabilities will not be known until a successful connection + /// has been made via the method. + /// + /// The capabilities. + public Pop3Capabilities Capabilities { + get; set; + } + + /// + /// Gets the underlying POP3 stream. + /// + /// + /// Gets the underlying POP3 stream. + /// + /// The pop3 stream. + public Pop3Stream Stream { + get { return stream; } + } + + /// + /// Gets or sets the state of the engine. + /// + /// + /// Gets or sets the state of the engine. + /// + /// The engine state. + public Pop3EngineState State { + get; internal set; + } + + /// + /// Gets whether or not the engine is currently connected to a POP3 server. + /// + /// + /// Gets whether or not the engine is currently connected to a POP3 server. + /// + /// true if the engine is connected; otherwise, false. + public bool IsConnected { + get { return stream != null && stream.IsConnected; } + } + + /// + /// Gets the APOP authentication token. + /// + /// + /// Gets the APOP authentication token. + /// + /// The APOP authentication token. + public string ApopToken { + get; private set; + } + + /// + /// Gets the EXPIRE extension policy value. + /// + /// + /// Gets the EXPIRE extension policy value. + /// + /// The EXPIRE policy. + public int ExpirePolicy { + get; private set; + } + + /// + /// Gets the implementation details of the server. + /// + /// + /// Gets the implementation details of the server. + /// + /// The implementation details. + public string Implementation { + get; private set; + } + + /// + /// Gets the login delay. + /// + /// + /// Gets the login delay. + /// + /// The login delay. + public int LoginDelay { + get; private set; + } + + async Task ConnectAsync (Pop3Stream pop3, bool doAsync, CancellationToken cancellationToken) + { + if (stream != null) + stream.Dispose (); + + Capabilities = Pop3Capabilities.User; + AuthenticationMechanisms.Clear (); + State = Pop3EngineState.Disconnected; + ApopToken = null; + stream = pop3; + + // read the pop3 server greeting + var greeting = (await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false)).TrimEnd (); + + int index = greeting.IndexOf (' '); + string token, text; + + if (index != -1) { + token = greeting.Substring (0, index); + + while (index < greeting.Length && char.IsWhiteSpace (greeting[index])) + index++; + + if (index < greeting.Length) + text = greeting.Substring (index); + else + text = string.Empty; + } else { + text = string.Empty; + token = greeting; + } + + if (token != "+OK") { + stream.Dispose (); + stream = null; + + throw new Pop3ProtocolException (string.Format ("Unexpected greeting from server: {0}", greeting)); + } + + index = text.IndexOf ('<'); + if (index != -1 && index + 1 < text.Length) { + int endIndex = text.IndexOf ('>', index + 1); + + if (endIndex++ != -1) { + ApopToken = text.Substring (index, endIndex - index); + Capabilities |= Pop3Capabilities.Apop; + } + } + + State = Pop3EngineState.Connected; + } + + /// + /// Takes posession of the and reads the greeting. + /// + /// + /// Takes posession of the and reads the greeting. + /// + /// The pop3 stream. + /// The cancellation token + public void Connect (Pop3Stream pop3, CancellationToken cancellationToken) + { + ConnectAsync (pop3, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Takes posession of the and reads the greeting. + /// + /// + /// Takes posession of the and reads the greeting. + /// + /// The pop3 stream. + /// The cancellation token + public Task ConnectAsync (Pop3Stream pop3, CancellationToken cancellationToken) + { + return ConnectAsync (pop3, true, cancellationToken); + } + + public event EventHandler Disconnected; + + void OnDisconnected () + { + var handler = Disconnected; + + if (handler != null) + handler (this, EventArgs.Empty); + } + + /// + /// Disconnects the . + /// + /// + /// Disconnects the . + /// + public void Disconnect () + { + if (stream != null) { + stream.Dispose (); + stream = null; + } + + if (State != Pop3EngineState.Disconnected) { + State = Pop3EngineState.Disconnected; + OnDisconnected (); + } + } + + async Task ReadLineAsync (bool doAsync, CancellationToken cancellationToken) + { + if (stream == null) + throw new InvalidOperationException (); + + using (var memory = new MemoryStream ()) { + bool complete; + byte[] buf; + int count; + + do { + if (doAsync) + complete = await stream.ReadLineAsync (memory, cancellationToken).ConfigureAwait (false); + else + complete = stream.ReadLine (memory, cancellationToken); + } while (!complete); + + count = (int) memory.Length; +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + buf = memory.GetBuffer (); +#else + buf = memory.ToArray (); +#endif + + // Trim the sequence from the end of the line. + if (buf[count - 1] == (byte) '\n') { + count--; + + if (buf[count - 1] == (byte) '\r') + count--; + } + + try { + return UTF8.GetString (buf, 0, count); + } catch (DecoderFallbackException) { + return Latin1.GetString (buf, 0, count); + } + } + } + + /// + /// Reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public string ReadLine (CancellationToken cancellationToken) + { + return ReadLineAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Reads a single line from the . + /// + /// The line. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task ReadLineAsync (CancellationToken cancellationToken) + { + return ReadLineAsync (true, cancellationToken); + } + + public static Pop3CommandStatus GetCommandStatus (string response, out string text) + { + int index = response.IndexOf (' '); + string token; + + if (index != -1) { + token = response.Substring (0, index); + + while (index < response.Length && char.IsWhiteSpace (response[index])) + index++; + + if (index < response.Length) + text = response.Substring (index); + else + text = string.Empty; + } else { + text = string.Empty; + token = response; + } + + if (token == "+OK") + return Pop3CommandStatus.Ok; + + if (token == "-ERR") + return Pop3CommandStatus.Error; + + if (token == "+") + return Pop3CommandStatus.Continue; + + return Pop3CommandStatus.ProtocolError; + } + + async Task SendCommandAsync (Pop3Command pc, bool doAsync, CancellationToken cancellationToken) + { + var buf = pc.Encoding.GetBytes (pc.Command + "\r\n"); + + if (doAsync) + await stream.WriteAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false); + else + stream.Write (buf, 0, buf.Length, cancellationToken); + } + + async Task ReadResponseAsync (Pop3Command pc, bool doAsync) + { + string response, text; + + try { + response = (await ReadLineAsync (doAsync, pc.CancellationToken).ConfigureAwait (false)).TrimEnd (); + } catch { + pc.Status = Pop3CommandStatus.ProtocolError; + Disconnect (); + throw; + } + + pc.Status = GetCommandStatus (response, out text); + pc.StatusText = text; + + switch (pc.Status) { + case Pop3CommandStatus.ProtocolError: + Disconnect (); + throw new Pop3ProtocolException (string.Format ("Unexpected response from server: {0}", response)); + case Pop3CommandStatus.Continue: + case Pop3CommandStatus.Ok: + if (pc.Handler != null) { + try { + await pc.Handler (this, pc, text, doAsync).ConfigureAwait (false); + } catch { + pc.Status = Pop3CommandStatus.ProtocolError; + Disconnect (); + throw; + } + } + break; + } + } + + async Task IterateAsync (bool doAsync) + { + if (stream == null) + throw new InvalidOperationException (); + + if (queue.Count == 0) + return 0; + + int count = (Capabilities & Pop3Capabilities.Pipelining) != 0 ? queue.Count : 1; + var cancellationToken = queue[0].CancellationToken; + var active = new List (); + + if (cancellationToken.IsCancellationRequested) { + queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); + cancellationToken.ThrowIfCancellationRequested (); + } + + for (int i = 0; i < count; i++) { + var pc = queue[0]; + + if (i > 0 && !pc.CancellationToken.Equals (cancellationToken)) + break; + + queue.RemoveAt (0); + + pc.Status = Pop3CommandStatus.Active; + active.Add (pc); + + await SendCommandAsync (pc, doAsync, cancellationToken).ConfigureAwait (false); + } + + if (doAsync) + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + else + stream.Flush (cancellationToken); + + for (int i = 0; i < active.Count; i++) + await ReadResponseAsync (active[i], doAsync).ConfigureAwait (false); + + return active[active.Count - 1].Id; + } + + /// + /// Iterate the command pipeline. + /// + /// The ID of the command that just completed. + public int Iterate () + { + return IterateAsync (false).GetAwaiter ().GetResult (); + } + + /// + /// Iterate the command pipeline. + /// + /// The ID of the command that just completed. + public Task IterateAsync () + { + return IterateAsync (true); + } + + public Pop3Command QueueCommand (CancellationToken cancellationToken, Pop3CommandHandler handler, Encoding encoding, string format, params object[] args) + { + var pc = new Pop3Command (cancellationToken, handler, encoding, format, args); + pc.Id = nextId++; + queue.Add (pc); + return pc; + } + + public Pop3Command QueueCommand (CancellationToken cancellationToken, Pop3CommandHandler handler, string format, params object[] args) + { + return QueueCommand (cancellationToken, handler, Encoding.ASCII, format, args); + } + + static async Task CapaHandler (Pop3Engine engine, Pop3Command pc, string text, bool doAsync) + { + if (pc.Status != Pop3CommandStatus.Ok) + return; + + string response; + + do { + if ((response = await engine.ReadLineAsync (doAsync, pc.CancellationToken).ConfigureAwait (false)) == ".") + break; + + int index = response.IndexOf (' '); + string token, data; + int value; + + if (index != -1) { + token = response.Substring (0, index); + + while (index < response.Length && char.IsWhiteSpace (response[index])) + index++; + + if (index < response.Length) + data = response.Substring (index); + else + data = string.Empty; + } else { + data = string.Empty; + token = response; + } + + switch (token) { + case "EXPIRE": + engine.Capabilities |= Pop3Capabilities.Expire; + var tokens = data.Split (' '); + + if (int.TryParse (tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out value)) + engine.ExpirePolicy = value; + else if (tokens[0] == "NEVER") + engine.ExpirePolicy = -1; + break; + case "IMPLEMENTATION": + engine.Implementation = data; + break; + case "LOGIN-DELAY": + if (int.TryParse (data, NumberStyles.None, CultureInfo.InvariantCulture, out value)) { + engine.Capabilities |= Pop3Capabilities.LoginDelay; + engine.LoginDelay = value; + } + break; + case "PIPELINING": + engine.Capabilities |= Pop3Capabilities.Pipelining; + break; + case "RESP-CODES": + engine.Capabilities |= Pop3Capabilities.ResponseCodes; + break; + case "SASL": + engine.Capabilities |= Pop3Capabilities.Sasl; + foreach (var authmech in data.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) + engine.AuthenticationMechanisms.Add (authmech); + break; + case "STLS": + engine.Capabilities |= Pop3Capabilities.StartTLS; + break; + case "TOP": + engine.Capabilities |= Pop3Capabilities.Top; + break; + case "UIDL": + engine.Capabilities |= Pop3Capabilities.UIDL; + break; + case "USER": + engine.Capabilities |= Pop3Capabilities.User; + break; + case "UTF8": + engine.Capabilities |= Pop3Capabilities.UTF8; + + foreach (var item in data.Split (' ')) { + if (item == "USER") + engine.Capabilities |= Pop3Capabilities.UTF8User; + } + break; + case "LANG": + engine.Capabilities |= Pop3Capabilities.Lang; + break; + } + } while (true); + } + + async Task QueryCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken) + { + if (stream == null) + throw new InvalidOperationException (); + + // Clear all CAPA response capabilities (except the APOP, USER, and STLS capabilities). + Capabilities &= Pop3Capabilities.Apop | Pop3Capabilities.User | Pop3Capabilities.StartTLS; + AuthenticationMechanisms.Clear (); + Implementation = null; + ExpirePolicy = 0; + LoginDelay = 0; + + var pc = QueueCommand (cancellationToken, CapaHandler, "CAPA"); + + while (await IterateAsync (doAsync).ConfigureAwait (false) < pc.Id) { + // continue processing commands... + } + + return pc.Status; + } + + public Pop3CommandStatus QueryCapabilities (CancellationToken cancellationToken) + { + return QueryCapabilitiesAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + public Task QueryCapabilitiesAsync (CancellationToken cancellationToken) + { + return QueryCapabilitiesAsync (true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Language.cs b/src/MailKit/Net/Pop3/Pop3Language.cs new file mode 100644 index 0000000..4e8e559 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Language.cs @@ -0,0 +1,71 @@ +// +// Pop3Language.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Pop3 { + /// + /// A POP3 language. + /// + /// + /// A POP3 language. + /// + public class Pop3Language + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + internal Pop3Language (string lang, string desc) + { + Language = lang; + Description = desc; + } + + /// + /// Get the language code. + /// + /// + /// Gets the language code. This is the value that should be given to + /// . + /// + /// The language. + public string Language { + get; private set; + } + + /// + /// Get the description. + /// + /// + /// Gets the description. + /// + /// The description. + public string Description { + get; private set; + } + } +} diff --git a/src/MailKit/Net/Pop3/Pop3ProtocolException.cs b/src/MailKit/Net/Pop3/Pop3ProtocolException.cs new file mode 100644 index 0000000..bf288e5 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3ProtocolException.cs @@ -0,0 +1,101 @@ +// +// Pop3ProtocolException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Pop3 { + /// + /// A POP3 protocol exception. + /// + /// + /// The exception that is thrown when there is an error communicating with a POP3 server. A + /// is typically fatal and requires the + /// to be reconnected. + /// + /// + /// + /// +#if SERIALIZABLE + [Serializable] +#endif + public class Pop3ProtocolException : ProtocolException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected Pop3ProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public Pop3ProtocolException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public Pop3ProtocolException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public Pop3ProtocolException () + { + } + } +} diff --git a/src/MailKit/Net/Pop3/Pop3Stream.cs b/src/MailKit/Net/Pop3/Pop3Stream.cs new file mode 100644 index 0000000..4ead936 --- /dev/null +++ b/src/MailKit/Net/Pop3/Pop3Stream.cs @@ -0,0 +1,950 @@ +// +// Pop3Stream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +using MimeKit.IO; + +using Buffer = System.Buffer; +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Pop3 { + /// + /// An enumeration of the possible POP3 streaming modes. + /// + /// + /// Normal operation is done in the mode, + /// but when retrieving messages (via RETR) or headers (via TOP), the + /// mode should be used. + /// + enum Pop3StreamMode { + /// + /// Reads 1 line at a time. + /// + Line, + + /// + /// Reads data in chunks, ignoring line state. + /// + Data + } + + /// + /// A stream for communicating with a POP3 server. + /// + /// + /// A stream capable of reading data line-by-line () + /// or by raw byte streams (). + /// + class Pop3Stream : Stream, ICancellableStream + { + const int ReadAheadSize = 128; + const int BlockSize = 4096; + const int PadSize = 4; + + // I/O buffering + readonly byte[] input = new byte[ReadAheadSize + BlockSize + PadSize]; + const int inputStart = ReadAheadSize; + + readonly byte[] output = new byte[BlockSize]; + int outputIndex; + + readonly IProtocolLogger logger; + int inputIndex = ReadAheadSize; + int inputEnd = ReadAheadSize; + Pop3StreamMode mode; + bool disposed; + bool midline; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The underlying network stream. + /// The protocol logger. + public Pop3Stream (Stream source, IProtocolLogger protocolLogger) + { + logger = protocolLogger; + IsConnected = true; + Stream = source; + } + + /// + /// Get or sets the underlying network stream. + /// + /// + /// Gets or sets the underlying network stream. + /// + /// The underlying network stream. + public Stream Stream { + get; internal set; + } + + /// + /// Gets or sets the mode used for reading. + /// + /// The mode. + public Pop3StreamMode Mode { + get { return mode; } + set { + IsEndOfData = false; + mode = value; + } + } + + /// + /// Get whether or not the stream is connected. + /// + /// + /// Gets whether or not the stream is connected. + /// + /// true if the stream is connected; otherwise, false. + public bool IsConnected { + get; private set; + } + + /// + /// Get whether or not the end of the raw data has been reached in mode. + /// + /// + /// When reading the resonse to a command such as RETR, the end of the data is marked by line matching ".\r\n". + /// + /// true if the end of the data has been reached; otherwise, false. + public bool IsEndOfData { + get; private set; + } + + /// + /// Get whether the stream supports reading. + /// + /// + /// Gets whether the stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return Stream.CanRead; } + } + + /// + /// Get whether the stream supports writing. + /// + /// + /// Gets whether the stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return Stream.CanWrite; } + } + + /// + /// Get whether the stream supports seeking. + /// + /// + /// Gets whether the stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Get whether the stream supports I/O timeouts. + /// + /// + /// Gets whether the stream supports I/O timeouts. + /// + /// true if the stream supports I/O timeouts; otherwise, false. + public override bool CanTimeout { + get { return Stream.CanTimeout; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// The read timeout. + public override int ReadTimeout { + get { return Stream.ReadTimeout; } + set { Stream.ReadTimeout = value; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// The write timeout. + public override int WriteTimeout { + get { return Stream.WriteTimeout; } + set { Stream.WriteTimeout = value; } + } + + /// + /// Get or set the position within the current stream. + /// + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return Stream.Position; } + set { throw new NotSupportedException (); } + } + + /// + /// Get the length of the stream, in bytes. + /// + /// + /// Gets the length of the stream, in bytes. + /// + /// A long value representing the length of the stream in bytes. + /// The length of the stream. + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Length { + get { return Stream.Length; } + } + + async Task ReadAheadAsync (bool doAsync, CancellationToken cancellationToken) + { + int left = inputEnd - inputIndex; + int start = inputStart; + int end = inputEnd; + int nread; + + if (left > 0) { + int index = inputIndex; + + // attempt to align the end of the remaining input with ReadAheadSize + if (index >= start) { + start -= Math.Min (ReadAheadSize, left); + Buffer.BlockCopy (input, index, input, start, left); + index = start; + start += left; + } else if (index > 0) { + int shift = Math.Min (index, end - start); + Buffer.BlockCopy (input, index, input, index - shift, left); + index -= shift; + start = index + left; + } else { + // we can't shift... + start = end; + } + + inputIndex = index; + inputEnd = start; + } else { + inputIndex = start; + inputEnd = start; + } + + end = input.Length - PadSize; + + try { + var network = Stream as NetworkStream; + + cancellationToken.ThrowIfCancellationRequested (); + + if (doAsync) { + nread = await Stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectRead, cancellationToken); + nread = Stream.Read (input, start, end - start); + } + + if (nread > 0) { + logger.LogServer (input, start, nread); + inputEnd += nread; + } else { + throw new Pop3ProtocolException ("The POP3 server has unexpectedly disconnected."); + } + } catch { + IsConnected = false; + throw; + } + + return inputEnd - inputIndex; + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (Pop3Stream)); + } + + bool NeedInput (int index, int inputLeft) + { + if (inputLeft == 2 && input[index] == (byte) '.' && input[index + 1] == '\n') + return false; + + return true; + } + + async Task ReadAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (Mode != Pop3StreamMode.Data) + throw new InvalidOperationException (); + + if (IsEndOfData || count == 0) + return 0; + + int end = offset + count; + int index = offset; + int inputLeft; + + do { + inputLeft = inputEnd - inputIndex; + + // we need at least 3 bytes: ".\r\n" + if (inputLeft < 3 && (midline || NeedInput (inputIndex, inputLeft))) { + if (index > offset) + break; + + await ReadAheadAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + // terminate the input buffer with a '\n' to remove bounds checking in our inner loop + input[inputEnd] = (byte) '\n'; + + while (inputIndex < inputEnd) { + if (midline) { + // read until end-of-line + while (index < end && input[inputIndex] != (byte) '\n') + buffer[index++] = input[inputIndex++]; + + if (inputIndex == inputEnd || index == end) + break; + + // consume the '\n' character + buffer[index++] = input[inputIndex++]; + midline = false; + } + + if (inputIndex == inputEnd) + break; + + if (input[inputIndex] == (byte) '.') { + inputLeft = inputEnd - inputIndex; + + // check for ".\r\n" which signifies the end of the data stream + if (inputLeft >= 3 && input[inputIndex + 1] == (byte) '\r' && input[inputIndex + 2] == (byte) '\n') { + IsEndOfData = true; + midline = false; + inputIndex += 3; + break; + } + + // check for ".\n" which is used by some broken UNIX servers in place of ".\r\n" + if (inputLeft >= 2 && input[inputIndex + 1] == (byte) '\n') { + IsEndOfData = true; + midline = false; + inputIndex += 2; + break; + } + + // check for "." or ".\r" which might be an incomplete termination sequence + if (inputLeft == 1 || (inputLeft == 2 && input[inputIndex + 1] == (byte) '\r')) { + // not enough data... + break; + } + + // check for lines beginning with ".." which should be transformed into "." + if (input[inputIndex + 1] == (byte) '.') + inputIndex++; + } + + midline = true; + } + } while (index < end && !IsEndOfData); + + return index - offset; + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream is in line mode (see ). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream is in line mode (see ). + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + return Read (buffer, offset, count, CancellationToken.None); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream is in line mode (see ). + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync (buffer, offset, count, true, cancellationToken); + } + + async Task ReadLineAsync (Stream ostream, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (inputIndex == inputEnd) + await ReadAheadAsync (doAsync, cancellationToken).ConfigureAwait (false); + + unsafe { + fixed (byte* inbuf = input) { + byte* start, inptr, inend; + int offset = inputIndex; + int count; + + start = inbuf + inputIndex; + inend = inbuf + inputEnd; + *inend = (byte) '\n'; + inptr = start; + + // FIXME: use SIMD to optimize this + while (*inptr != (byte) '\n') + inptr++; + + inputIndex = (int) (inptr - inbuf); + count = (int) (inptr - start); + + if (inptr == inend) { + ostream.Write (input, offset, count); + midline = true; + return false; + } + + // consume the '\n' + midline = false; + inputIndex++; + count++; + + ostream.Write (input, offset, count); + + return true; + } + } + } + + /// + /// Reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns true. + /// + /// true, if reading the line is complete, false otherwise. + /// The output stream to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal bool ReadLine (Stream ostream, CancellationToken cancellationToken) + { + return ReadLineAsync (ostream, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously reads a single line of input from the stream. + /// + /// + /// This method should be called in a loop until it returns true. + /// + /// true, if reading the line is complete, false otherwise. + /// The output stream to write the line data into. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + internal Task ReadLineAsync (Stream ostream, CancellationToken cancellationToken) + { + return ReadLineAsync (ostream, true, cancellationToken); + } + + async Task WriteAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + var network = NetworkStream.Get (Stream); + int index = offset; + int left = count; + + while (left > 0) { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; + } + + if (outputIndex == BlockSize) { + // flush the output buffer + if (doAsync) { + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, BlockSize); + } + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + if (doAsync) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (buffer, index, BlockSize); + } + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + + IsEndOfData = false; + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + Write (buffer, offset, count, CancellationToken.None); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync (buffer, offset, count, true, cancellationToken); + } + + async Task FlushAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + if (doAsync) { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, outputIndex); + Stream.Flush (); + } + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Flush (CancellationToken cancellationToken) + { + FlushAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + Flush (CancellationToken.None); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + return FlushAsync (true, cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + /// + /// Sets the length of the stream. + /// + /// The desired length of the stream in bytes. + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + IsConnected = false; + Stream.Dispose (); + } + + disposed = true; + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/Proxy/HttpProxyClient.cs b/src/MailKit/Net/Proxy/HttpProxyClient.cs new file mode 100644 index 0000000..def93e0 --- /dev/null +++ b/src/MailKit/Net/Proxy/HttpProxyClient.cs @@ -0,0 +1,248 @@ +// +// HttpProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// An HTTP proxy client. + /// + /// + /// An HTTP proxy client. + /// + public class HttpProxyClient : ProxyClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpProxyClient (string host, int port) : base (host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public HttpProxyClient (string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + } + + async Task ConnectAsync (string host, int port, bool doAsync, CancellationToken cancellationToken) + { + ValidateArguments (host, port); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, doAsync, cancellationToken).ConfigureAwait (false); + var builder = new StringBuilder (); + + builder.AppendFormat (CultureInfo.InvariantCulture, "CONNECT {0}:{1} HTTP/1.1\r\n", host, port); + builder.AppendFormat (CultureInfo.InvariantCulture, "Host: {0}:{1}\r\n", host, port); + if (ProxyCredentials != null) { + var token = Encoding.UTF8.GetBytes (string.Format (CultureInfo.InvariantCulture, "{0}:{1}", ProxyCredentials.UserName, ProxyCredentials.Password)); + var base64 = Convert.ToBase64String (token); + builder.AppendFormat (CultureInfo.InvariantCulture, "Proxy-Authorization: Basic {0}\r\n", base64); + } + builder.Append ("\r\n"); + + var command = Encoding.UTF8.GetBytes (builder.ToString ()); + + try { + await SendAsync (socket, command, 0, command.Length, doAsync, cancellationToken).ConfigureAwait (false); + + var buffer = new byte[1024]; + var endOfHeaders = false; + var newline = false; + + builder.Clear (); + + // read until we consume the end of the headers (it's ok if we read some of the content) + do { + int nread = await ReceiveAsync (socket, buffer, 0, buffer.Length, doAsync, cancellationToken).ConfigureAwait (false); + + if (nread > 0) { + int n = nread; + + for (int i = 0; i < nread && !endOfHeaders; i++) { + switch ((char) buffer[i]) { + case '\r': + break; + case '\n': + endOfHeaders = newline; + newline = true; + + if (endOfHeaders) + n = i + 1; + break; + default: + newline = false; + break; + } + } + + builder.Append (Encoding.UTF8.GetString (buffer, 0, n)); + } + } while (!endOfHeaders); + + int index = 0; + + while (builder[index] != '\n') + index++; + + if (index > 0 && builder[index - 1] == '\r') + index--; + + // trim everything beyond the "HTTP/1.1 200 ..." part of the response + builder.Length = index; + + var response = builder.ToString (); + + if (response.Length >= 15 && response.StartsWith ("HTTP/1.", StringComparison.OrdinalIgnoreCase) && + (response[7] == '1' || response[7] == '0') && response[8] == ' ' && + response[9] == '2' && response[10] == '0' && response[11] == '0' && + response[12] == ' ') { + return socket; + } + + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, response)); + } catch { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (socket.Connected) + socket.Disconnect (false); +#endif + socket.Dispose (); + throw; + } + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Socket Connect (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Proxy/IProxyClient.cs b/src/MailKit/Net/Proxy/IProxyClient.cs new file mode 100644 index 0000000..78cbfcc --- /dev/null +++ b/src/MailKit/Net/Proxy/IProxyClient.cs @@ -0,0 +1,208 @@ +// +// IProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// An interface for connecting to services via a proxy. + /// + /// + /// Implemented by + /// and . + /// + public interface IProxyClient + { + /// + /// Gets the proxy credentials. + /// + /// + /// Gets the credentials to use when authenticating with the proxy server. + /// + /// The proxy credentials. + NetworkCredential ProxyCredentials { get; } + + /// + /// Get the proxy host. + /// + /// + /// Gets the host name of the proxy server. + /// + /// The host name of the proxy server. + string ProxyHost { get; } + + /// + /// Get the proxy port. + /// + /// + /// Gets the port to use when connecting to the proxy server. + /// + /// The proxy port. + int ProxyPort { get; } + + /// + /// Get or set the local IP end point to use when connecting to a remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to a remote host. + /// + /// The local IP end point or null to use the default end point. + IPEndPoint LocalEndPoint { get; set; } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Socket Connect (string host, int port, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Socket Connect (string host, int port, int timeout, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + Task ConnectAsync (string host, int port, int timeout, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/Net/Proxy/ProxyClient.cs b/src/MailKit/Net/Proxy/ProxyClient.cs new file mode 100644 index 0000000..86c4723 --- /dev/null +++ b/src/MailKit/Net/Proxy/ProxyClient.cs @@ -0,0 +1,406 @@ +// +// ProxyClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// An abstract proxy client base class. + /// + /// + /// A proxy client can be used to connect to a service through a firewall that + /// would otherwise be blocked. + /// + public abstract class ProxyClient : IProxyClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + protected ProxyClient (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0 || host.Length > 255) + throw new ArgumentException ("The length of the host name must be between 0 and 256 characters.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + ProxyHost = host; + ProxyPort = port == 0 ? 1080 : port; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + protected ProxyClient (string host, int port, NetworkCredential credentials) : this (host, port) + { + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + ProxyCredentials = credentials; + } + + /// + /// Gets the proxy credentials. + /// + /// + /// Gets the credentials to use when authenticating with the proxy server. + /// + /// The proxy credentials. + public NetworkCredential ProxyCredentials { + get; private set; + } + + /// + /// Get the proxy host. + /// + /// + /// Gets the host name of the proxy server. + /// + /// The host name of the proxy server. + public string ProxyHost { + get; private set; + } + + /// + /// Get the proxy port. + /// + /// + /// Gets the port to use when connecting to the proxy server. + /// + /// The proxy port. + public int ProxyPort { + get; private set; + } + + /// + /// Get or set the local IP end point to use when connecting to a remote host. + /// + /// + /// Gets or sets the local IP end point to use when connecting to a remote host. + /// + /// The local IP end point or null to use the default end point. + public IPEndPoint LocalEndPoint { + get; set; + } + + internal static void ValidateArguments (string host, int port) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0 || host.Length > 255) + throw new ArgumentException ("The length of the host name must be between 0 and 256 characters.", nameof (host)); + + if (port <= 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + } + + static void ValidateArguments (string host, int port, int timeout) + { + ValidateArguments (host, port); + + if (timeout < -1) + throw new ArgumentOutOfRangeException (nameof (timeout)); + } + + static void AsyncOperationCompleted (object sender, SocketAsyncEventArgs args) + { + var tcs = (TaskCompletionSource) args.UserToken; + + if (args.SocketError == SocketError.Success) { + tcs.TrySetResult (true); + return; + } + + tcs.TrySetException (new SocketException ((int) args.SocketError)); + } + + internal static async Task SendAsync (Socket socket, byte[] buffer, int offset, int length, bool doAsync, CancellationToken cancellationToken) + { + if (doAsync || cancellationToken.CanBeCanceled) { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.SendAsync (args)) + AsyncOperationCompleted (null, args); + + if (doAsync) + await tcs.Task.ConfigureAwait (false); + else + tcs.Task.GetAwaiter ().GetResult (); + + return; + } + } + } + + SocketUtils.Poll (socket, SelectMode.SelectWrite, cancellationToken); + + socket.Send (buffer, offset, length, SocketFlags.None); + } + + internal static async Task ReceiveAsync (Socket socket, byte[] buffer, int offset, int length, bool doAsync, CancellationToken cancellationToken) + { + if (doAsync || cancellationToken.CanBeCanceled) { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + using (var args = new SocketAsyncEventArgs ()) { + args.Completed += AsyncOperationCompleted; + args.SetBuffer (buffer, offset, length); + args.AcceptSocket = socket; + args.UserToken = tcs; + + if (!socket.ReceiveAsync (args)) + AsyncOperationCompleted (null, args); + + if (doAsync) + await tcs.Task.ConfigureAwait (false); + else + tcs.Task.GetAwaiter ().GetResult (); + + return args.BytesTransferred; + } + } + } + + SocketUtils.Poll (socket, SelectMode.SelectRead, cancellationToken); + + return socket.Receive (buffer, offset, length, SocketFlags.None); + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public abstract Socket Connect (string host, int port, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public abstract Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// -or- + /// is less than -1. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public virtual Socket Connect (string host, int port, int timeout, CancellationToken cancellationToken = default (CancellationToken)) + { + ValidateArguments (host, port, timeout); + + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return Connect (host, port, linked.Token); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The timeout, in milliseconds. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// -or- + /// is less than -1. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// The operation timed out. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public async virtual Task ConnectAsync (string host, int port, int timeout, CancellationToken cancellationToken = default (CancellationToken)) + { + ValidateArguments (host, port, timeout); + + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return await ConnectAsync (host, port, linked.Token).ConfigureAwait (false); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + } +} diff --git a/src/MailKit/Net/Proxy/ProxyProtocolException.cs b/src/MailKit/Net/Proxy/ProxyProtocolException.cs new file mode 100644 index 0000000..0e1ee47 --- /dev/null +++ b/src/MailKit/Net/Proxy/ProxyProtocolException.cs @@ -0,0 +1,97 @@ +// +// ProxyProtocolException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Proxy +{ + /// + /// A proxy protocol exception. + /// + /// + /// The exception that is thrown when there is an error communicating with a proxy server. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ProxyProtocolException : ProtocolException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected ProxyProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public ProxyProtocolException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public ProxyProtocolException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public ProxyProtocolException () + { + } + } +} diff --git a/src/MailKit/Net/Proxy/Socks4Client.cs b/src/MailKit/Net/Proxy/Socks4Client.cs new file mode 100644 index 0000000..bcd3969 --- /dev/null +++ b/src/MailKit/Net/Proxy/Socks4Client.cs @@ -0,0 +1,299 @@ +// +// Socks4Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS4 proxy client. + /// + /// + /// A SOCKS4 proxy client. + /// + public class Socks4Client : SocksClient + { + static readonly byte[] InvalidIPAddress = { 0, 0, 0, 1 }; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks4Client (string host, int port) : base (4, host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + public Socks4Client (string host, int port, NetworkCredential credentials) : base (4, host, port, credentials) + { + } + + /// + /// Get or set whether this is a Socks4a client. + /// + /// + /// Gets or sets whether this is a Socks4a client. + /// + /// true if is is a Socks4a client; otherwise, false. + protected bool IsSocks4a { + get; set; + } + + enum Socks4Command : byte + { + Connect = 0x01, + Bind = 0x02, + } + + enum Socks4Reply : byte + { + RequestGranted = 0x5a, + RequestRejected = 0x5b, + RequestFailedNoIdentd = 0x5c, + RequestFailedWrongId = 0x5d + } + + static string GetFailureReason (byte reply) + { + switch ((Socks4Reply) reply) { + case Socks4Reply.RequestRejected: return "Request rejected or failed."; + case Socks4Reply.RequestFailedNoIdentd: return "Request failed; unable to contact client machine's identd service."; + case Socks4Reply.RequestFailedWrongId: return "Request failed; client ID does not match specified username."; + default: return "Unknown error."; + } + } + + async Task ResolveAsync (string host, bool doAsync, CancellationToken cancellationToken) + { + IPAddress[] ipAddresses; + + if (doAsync) { + ipAddresses = await Dns.GetHostAddressesAsync (host).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ipAddresses = Dns.GetHostAddressesAsync (host).GetAwaiter ().GetResult (); +#else + ipAddresses = Dns.GetHostAddresses (host); +#endif + } + + for (int i = 0; i < ipAddresses.Length; i++) { + if (ipAddresses[i].AddressFamily == AddressFamily.InterNetwork) + return ipAddresses[i]; + } + + throw new ArgumentException ($"Could not resolve a suitable IPv4 address for '{host}'.", nameof (host)); + } + + async Task ConnectAsync (string host, int port, bool doAsync, CancellationToken cancellationToken) + { + byte[] addr, domain = null; + IPAddress ip; + + ValidateArguments (host, port); + + if (!IPAddress.TryParse (host, out ip)) { + if (IsSocks4a) { + domain = Encoding.UTF8.GetBytes (host); + addr = InvalidIPAddress; + } else { + ip = await ResolveAsync (host, doAsync, cancellationToken).ConfigureAwait (false); + addr = ip.GetAddressBytes (); + } + } else { + if (ip.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException ("The specified host address must be IPv4.", nameof (host)); + + addr = ip.GetAddressBytes (); + } + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, doAsync, cancellationToken).ConfigureAwait (false); + var user = ProxyCredentials != null ? Encoding.UTF8.GetBytes (ProxyCredentials.UserName) : new byte[0]; + + try { + // +----+-----+----------+----------+----------+-------+--------------+-------+ + // |VER | CMD | DST.PORT | DST.ADDR | USERID | NULL | DST.DOMAIN | NULL | + // +----+-----+----------+----------+----------+-------+--------------+-------+ + // | 1 | 1 | 2 | 4 | VARIABLE | X'00' | VARIABLE | X'00' | + // +----+-----+----------+----------+----------+-------+--------------+-------+ + int bufferSize = 9 + user.Length + (domain != null ? domain.Length + 1 : 0); + var buffer = new byte[bufferSize]; + int nread, n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) Socks4Command.Connect; + buffer[n++] = (byte)(port >> 8); + buffer[n++] = (byte) port; + Buffer.BlockCopy (addr, 0, buffer, n, 4); + n += 4; + Buffer.BlockCopy (user, 0, buffer, n, user.Length); + n += user.Length; + buffer[n++] = 0x00; + if (domain != null) { + Buffer.BlockCopy (domain, 0, buffer, n, domain.Length); + n += domain.Length; + buffer[n++] = 0x00; + } + + await SendAsync (socket, buffer, 0, n, doAsync, cancellationToken).ConfigureAwait (false); + + // +-----+-----+----------+----------+ + // | VER | REP | BND.PORT | BND.ADDR | + // +-----+-----+----------+----------+ + // | 1 | 1 | 2 | 4 | + // +-----+-----+----------+----------+ + n = 0; + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 8 - n, doAsync, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 8); + + if (buffer[1] != (byte) Socks4Reply.RequestGranted) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, GetFailureReason (buffer[1]))); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return socket; + } catch { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (socket.Connected) + socket.Disconnect (false); +#endif + socket.Dispose (); + throw; + } + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Socket Connect (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Proxy/Socks4aClient.cs b/src/MailKit/Net/Proxy/Socks4aClient.cs new file mode 100644 index 0000000..463db6e --- /dev/null +++ b/src/MailKit/Net/Proxy/Socks4aClient.cs @@ -0,0 +1,88 @@ +// +// Socks4aClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Net; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS4a proxy client. + /// + /// + /// A SOCKS4a proxy client. + /// + public class Socks4aClient : Socks4Client + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks4aClient (string host, int port) : base (host, port) + { + IsSocks4a = true; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + public Socks4aClient (string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + IsSocks4a = true; + } + } +} diff --git a/src/MailKit/Net/Proxy/Socks5Client.cs b/src/MailKit/Net/Proxy/Socks5Client.cs new file mode 100644 index 0000000..54a2fb6 --- /dev/null +++ b/src/MailKit/Net/Proxy/Socks5Client.cs @@ -0,0 +1,421 @@ +// +// Socks5Client.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Globalization; +using System.Threading.Tasks; + +using MailKit.Security; + +namespace MailKit.Net.Proxy +{ + /// + /// A SOCKS5 proxy client. + /// + /// + /// A SOCKS5 proxy client. + /// + public class Socks5Client : SocksClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks5Client (string host, int port) : base (5, host, port) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// -or- + /// The length of is greater than 255 characters. + /// + public Socks5Client (string host, int port, NetworkCredential credentials) : base (5, host, port, credentials) + { + } + + internal enum Socks5AddressType : byte + { + None = 0x00, + IPv4 = 0x01, + Domain = 0x03, + IPv6 = 0x04 + } + + enum Socks5AuthMethod : byte + { + Anonymous = 0x00, + GSSAPI = 0x01, + UserPassword = 0x02, + NotSupported = 0xff + } + + enum Socks5Command : byte + { + Connect = 0x01, + Bind = 0x02, + UdpAssociate = 0x03, + } + + internal enum Socks5Reply : byte + { + Success = 0x00, + GeneralServerFailure = 0x01, + ConnectionNotAllowed = 0x02, + NetworkUnreachable = 0x03, + HostUnreachable = 0x04, + ConnectionRefused = 0x05, + TTLExpired = 0x06, + CommandNotSupported = 0x07, + AddressTypeNotSupported = 0x08 + } + + internal static string GetFailureReason (byte reply) + { + switch ((Socks5Reply) reply) { + case Socks5Reply.GeneralServerFailure: return "General server failure."; + case Socks5Reply.ConnectionNotAllowed: return "Connection not allowed."; + case Socks5Reply.NetworkUnreachable: return "Network unreachable."; + case Socks5Reply.HostUnreachable: return "Host unreachable."; + case Socks5Reply.ConnectionRefused: return "Connection refused."; + case Socks5Reply.TTLExpired: return "TTL expired."; + case Socks5Reply.CommandNotSupported: return "Command not supported."; + case Socks5Reply.AddressTypeNotSupported: return "Address type not supported."; + default: return string.Format (CultureInfo.InvariantCulture, "Unknown error ({0}).", (int) reply); + } + } + + internal static Socks5AddressType GetAddressType (string host, out IPAddress ip) + { + if (!IPAddress.TryParse (host, out ip)) + return Socks5AddressType.Domain; + + switch (ip.AddressFamily) { + case AddressFamily.InterNetworkV6: return Socks5AddressType.IPv6; + case AddressFamily.InterNetwork: return Socks5AddressType.IPv4; + default: throw new ArgumentException ("The host address must be an IPv4 or IPv6 address.", nameof (host)); + } + } + + void VerifySocksVersion (byte version) + { + if (version != (byte) SocksVersion) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Proxy server responded with unknown SOCKS version: {0}", (int) version)); + } + + async Task NegotiateAuthMethodAsync (Socket socket, bool doAsync, CancellationToken cancellationToken, params Socks5AuthMethod[] methods) + { + // +-----+----------+----------+ + // | VER | NMETHODS | METHODS | + // +-----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +-----+----------+----------+ + var buffer = new byte[2 + methods.Length]; + int nread, n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) methods.Length; + for (int i = 0; i < methods.Length; i++) + buffer[n++] = (byte) methods[i]; + + await SendAsync (socket, buffer, 0, n, doAsync, cancellationToken).ConfigureAwait (false); + + // +-----+--------+ + // | VER | METHOD | + // +-----+--------+ + // | 1 | 1 | + // +-----+--------+ + n = 0; + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 2 - n, doAsync, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 2); + + VerifySocksVersion (buffer[0]); + + return (Socks5AuthMethod) buffer[1]; + } + + async Task AuthenticateAsync (Socket socket, bool doAsync, CancellationToken cancellationToken) + { + var user = Encoding.UTF8.GetBytes (ProxyCredentials.UserName); + + if (user.Length > 255) + throw new AuthenticationException ("User name too long."); + + var passwd = Encoding.UTF8.GetBytes (ProxyCredentials.Password); + + if (passwd.Length > 255) { + Array.Clear (passwd, 0, passwd.Length); + throw new AuthenticationException ("Password too long."); + } + + var buffer = new byte[user.Length + passwd.Length + 3]; + int nread, n = 0; + + buffer[n++] = 1; + buffer[n++] = (byte) user.Length; + Buffer.BlockCopy (user, 0, buffer, n, user.Length); + n += user.Length; + buffer[n++] = (byte) passwd.Length; + Buffer.BlockCopy (passwd, 0, buffer, n, passwd.Length); + n += passwd.Length; + + Array.Clear (passwd, 0, passwd.Length); + + await SendAsync (socket, buffer, 0, n, doAsync, cancellationToken).ConfigureAwait (false); + + n = 0; + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, 2 - n, doAsync, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < 2); + + if (buffer[1] != (byte) Socks5Reply.Success) + throw new AuthenticationException ("Failed to authenticate with SOCKS5 proxy server."); + } + + async Task ConnectAsync (string host, int port, bool doAsync, CancellationToken cancellationToken) + { + Socks5AddressType addrType; + IPAddress ip; + + ValidateArguments (host, port); + + addrType = GetAddressType (host, out ip); + + cancellationToken.ThrowIfCancellationRequested (); + + var socket = await SocketUtils.ConnectAsync (ProxyHost, ProxyPort, LocalEndPoint, doAsync, cancellationToken).ConfigureAwait (false); + byte[] domain = null, addr = null; + const int bufferSize = 6 + 257; + + if (addrType == Socks5AddressType.Domain) + domain = Encoding.UTF8.GetBytes (host); + + try { + Socks5AuthMethod method; + + if (ProxyCredentials != null) + method = await NegotiateAuthMethodAsync (socket, doAsync, cancellationToken, Socks5AuthMethod.UserPassword, Socks5AuthMethod.Anonymous).ConfigureAwait (false); + else + method = await NegotiateAuthMethodAsync (socket, doAsync, cancellationToken, Socks5AuthMethod.Anonymous).ConfigureAwait (false); + + switch (method) { + case Socks5AuthMethod.UserPassword: + await AuthenticateAsync (socket, doAsync, cancellationToken).ConfigureAwait (false); + break; + case Socks5AuthMethod.Anonymous: + break; + default: + throw new ProxyProtocolException ("Failed to negotiate authentication method with the proxy server."); + } + + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + var buffer = new byte[bufferSize]; + int nread, n = 0; + + buffer[n++] = (byte) SocksVersion; + buffer[n++] = (byte) Socks5Command.Connect; + buffer[n++] = 0x00; + buffer[n++] = (byte) addrType; + switch (addrType) { + case Socks5AddressType.Domain: + buffer[n++] = (byte) domain.Length; + Buffer.BlockCopy (domain, 0, buffer, n, domain.Length); + n += domain.Length; + break; + case Socks5AddressType.IPv6: + addr = ip.GetAddressBytes (); + Buffer.BlockCopy (addr, 0, buffer, n, addr.Length); + n += 16; + break; + case Socks5AddressType.IPv4: + addr = ip.GetAddressBytes (); + Buffer.BlockCopy (addr, 0, buffer, n, addr.Length); + n += 4; + break; + } + buffer[n++] = (byte)(port >> 8); + buffer[n++] = (byte) port; + + await SendAsync (socket, buffer, 0, n, doAsync, cancellationToken).ConfigureAwait (false); + + // +-----+-----+-------+------+----------+----------+ + // | VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +-----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +-----+-----+-------+------+----------+----------+ + + // Note: We know we'll need at least 4 bytes of header + a minimum of 1 byte + // to determine the length of the BND.ADDR field if ATYP is a domain. + int need = 5; + n = 0; + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, need - n, doAsync, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < need); + + VerifySocksVersion (buffer[0]); + + if (buffer[1] != (byte) Socks5Reply.Success) + throw new ProxyProtocolException (string.Format (CultureInfo.InvariantCulture, "Failed to connect to {0}:{1}: {2}", host, port, GetFailureReason (buffer[1]))); + + addrType = (Socks5AddressType) buffer[3]; + + switch (addrType) { + case Socks5AddressType.Domain: need += buffer[4] + 2; break; + case Socks5AddressType.IPv6: need += (16 - 1) + 2; break; + case Socks5AddressType.IPv4: need += (4 - 1) + 2; break; + default: throw new ProxyProtocolException ("Proxy server returned unknown address type."); + } + + do { + if ((nread = await ReceiveAsync (socket, buffer, 0 + n, need - n, doAsync, cancellationToken).ConfigureAwait (false)) > 0) + n += nread; + } while (n < need); + + // TODO: do we care about BND.ADDR and BND.PORT? + + return socket; + } catch { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (socket.Connected) + socket.Disconnect (false); +#endif + socket.Dispose (); + throw; + } + } + + /// + /// Connect to the target host. + /// + /// + /// Connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Socket Connect (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously connect to the target host. + /// + /// + /// Asynchronously connects to the target host and port through the proxy server. + /// + /// The connected socket. + /// The host name of the proxy server. + /// The proxy server port. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An I/O error occurred. + /// + public override Task ConnectAsync (string host, int port, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Proxy/SocksClient.cs b/src/MailKit/Net/Proxy/SocksClient.cs new file mode 100644 index 0000000..4c91345 --- /dev/null +++ b/src/MailKit/Net/Proxy/SocksClient.cs @@ -0,0 +1,100 @@ +// +// SocksClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; + +namespace MailKit.Net.Proxy +{ + /// + /// An abstract SOCKS proxy client. + /// + /// + /// An abstract SOCKS proxy client. + /// + public abstract class SocksClient : ProxyClient + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The SOCKS protocol version. + /// The host name of the proxy server. + /// The proxy server port. + /// + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + protected SocksClient (int version, string host, int port) : base (host, port) + { + SocksVersion = version; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Initializes a new instance of the class. + /// + /// The SOCKS protocol version. + /// The host name of the proxy server. + /// The proxy server port. + /// The credentials to use to authenticate with the proxy server. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 1 and 65535. + /// + /// + /// The is a zero-length string. + /// + protected SocksClient (int version, string host, int port, NetworkCredential credentials) : base (host, port, credentials) + { + SocksVersion = version; + } + + /// + /// Get the SOCKS protocol version. + /// + /// + /// Gets the SOCKS protocol version. + /// + /// The SOCKS protocol version. + public int SocksVersion { + get; private set; + } + } +} diff --git a/src/MailKit/Net/SelectMode.cs b/src/MailKit/Net/SelectMode.cs new file mode 100644 index 0000000..893ea9f --- /dev/null +++ b/src/MailKit/Net/SelectMode.cs @@ -0,0 +1,36 @@ +// +// SelectMode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net +{ + enum SelectMode { + SelectRead, + SelectWrite, + SelectError + } +} diff --git a/src/MailKit/Net/Smtp/AsyncSmtpClient.cs b/src/MailKit/Net/Smtp/AsyncSmtpClient.cs new file mode 100644 index 0000000..603bcbc --- /dev/null +++ b/src/MailKit/Net/Smtp/AsyncSmtpClient.cs @@ -0,0 +1,689 @@ +// +// AsyncSmtpClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit; + +using MailKit.Security; + +namespace MailKit.Net.Smtp +{ + public partial class SmtpClient + { + /// + /// Asynchronously send a custom command to the SMTP server. + /// + /// + /// Asynchronously sends a custom command to the SMTP server. + /// The command string should not include the terminating \r\n sequence. + /// + /// The command response. + /// The command. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + protected Task SendCommandAsync (string command, CancellationToken cancellationToken = default (CancellationToken)) + { + if (command == null) + throw new ArgumentNullException (nameof (command)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can send commands."); + + return SendCommandAsync (command, true, cancellationToken); + } + + /// + /// Asynchronously authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// An asynchronous task context. + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (mechanism, true, cancellationToken); + } + + /// + /// Asynchronously authenticate using the supplied credentials. + /// + /// + /// If the SMTP server supports authentication, then the SASL mechanisms + /// that both the client and server support are tried in order of greatest + /// security to weakest security. Once a SASL authentication mechanism is + /// found that both client and server support, the credentials are used to + /// authenticate. + /// If, on the other hand, authentication is not supported by the SMTP + /// server, then this method will throw . + /// The property can be checked for the + /// flag to make sure the + /// SMTP server supports authentication before calling this method. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// An asynchronous task context. + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + return AuthenticateAsync (encoding, credentials, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 465. All other values will use a default port of 25. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// The connection established by any of the + /// Connect + /// methods may be re-used if an application wishes to send multiple messages + /// to the same SMTP server. Since connecting and authenticating can be expensive + /// operations, re-using a connection can significantly improve performance when + /// sending a large number of messages to the same SMTP server over a short + /// period of time. + /// + /// + /// + /// + /// An asynchronous task context. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (socket, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// An asynchronous task context. + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + return ConnectAsync (stream, host, port, options, true, cancellationToken); + } + + /// + /// Asynchronously disconnect the service. + /// + /// + /// If is true, a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// An asynchronous task context. + /// If set to true, a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override Task DisconnectAsync (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + return DisconnectAsync (quit, true, cancellationToken); + } + + /// + /// Asynchronously ping the SMTP server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// An asynchronous task context. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override Task NoOpAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + return NoOpAsync (true, cancellationToken); + } + + /// + /// Asynchronously send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// + /// + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override Task SendAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + var recipients = GetMessageRecipients (message); + var sender = GetMessageSender (message); + + if (sender == null) + throw new InvalidOperationException ("No sender has been specified."); + + if (recipients.Count == 0) + throw new InvalidOperationException ("No recipients have been specified."); + + return SendAsync (options, message, sender, recipients, true, cancellationToken, progress); + } + + /// + /// Asynchronously send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the message by uploading it to an SMTP server using the supplied sender and recipients. + /// + /// An asynchronous task context. + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (sender == null) + throw new ArgumentNullException (nameof (sender)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + var unique = new HashSet (StringComparer.OrdinalIgnoreCase); + var rcpts = new List (); + + AddUnique (rcpts, unique, recipients); + + if (rcpts.Count == 0) + throw new InvalidOperationException ("No recipients have been specified."); + + return SendAsync (options, message, sender, rcpts, true, cancellationToken, progress); + } + + /// + /// Asynchronously expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public Task ExpandAsync (string alias, CancellationToken cancellationToken = default (CancellationToken)) + { + return ExpandAsync (alias, true, cancellationToken); + } + + /// + /// Asynchronously verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public Task VerifyAsync (string address, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (address, true, cancellationToken); + } + } +} diff --git a/src/MailKit/Net/Smtp/ISmtpClient.cs b/src/MailKit/Net/Smtp/ISmtpClient.cs new file mode 100644 index 0000000..33eb72f --- /dev/null +++ b/src/MailKit/Net/Smtp/ISmtpClient.cs @@ -0,0 +1,246 @@ +// +// ISmtpClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; + +using MimeKit; + +namespace MailKit.Net.Smtp { + /// + /// An interface for an SMTP client. + /// + /// + /// Implemented by . + /// + public interface ISmtpClient : IMailTransport + { + /// + /// Get the capabilities supported by the SMTP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + SmtpCapabilities Capabilities { get; } + + /// + /// Get or set the local domain. + /// + /// + /// The local domain is used in the HELO or EHLO commands sent to + /// the SMTP server. If left unset, the local IP address will be + /// used instead. + /// + /// The local domain. + string LocalDomain { get; set; } + + /// + /// Get the maximum message size supported by the server. + /// + /// + /// The maximum message size will not be known until a successful connection has + /// been made and may change once the client is authenticated. + /// This value is only relevant if the includes + /// the flag. + /// + /// + /// + /// + /// The maximum message size supported by the server. + uint MaxSize { get; } + + /// + /// Get or set how much of the message to include in any failed delivery status notifications. + /// + /// + /// Gets or sets how much of the message to include in any failed delivery status notifications. + /// + /// A value indicating how much of the message to include in a failure delivery status notification. + DeliveryStatusNotificationType DeliveryStatusNotificationType { get; set; } + + /// + /// Expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + InternetAddressList Expand (string alias, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + Task ExpandAsync (string alias, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + MailboxAddress Verify (string address, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + Task VerifyAsync (string address, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MailKit/Net/Smtp/SmtpCapabilities.cs b/src/MailKit/Net/Smtp/SmtpCapabilities.cs new file mode 100644 index 0000000..d0b664d --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpCapabilities.cs @@ -0,0 +1,106 @@ +// +// SmtpCapabilities.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitations the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Net.Smtp { + /// + /// Capabilities supported by an SMTP server. + /// + /// + /// Capabilities are read as part of the response to the EHLO command that + /// is issued during the connection phase of the . + /// + /// + /// + /// + [Flags] + public enum SmtpCapabilities : uint { + /// + /// The server does not support any additional extensions. + /// + None = 0, + + /// + /// The server supports the SIZE extension + /// and may have a maximum message size limitation (see ). + /// + Size = 1 << 0, + + /// + /// The server supports the DSN extension, + /// allowing clients to specify which (if any) recipients they would like to receive delivery + /// notifications for. + /// + Dsn = 1 << 1, + + /// + /// The server supports the ENHANCEDSTATUSCODES + /// extension. + /// + EnhancedStatusCodes = 1 << 2, + + /// + /// The server supports the AUTH extension, + /// allowing clients to authenticate via supported SASL mechanisms. + /// + Authentication = 1 << 3, + + /// + /// The server supports the 8BITMIME extension, + /// allowing clients to send messages using the "8bit" Content-Transfer-Encoding. + /// + EightBitMime = 1 << 4, + + /// + /// The server supports the PIPELINING extension, + /// allowing clients to send multiple commands at once in order to reduce round-trip latency. + /// + Pipelining = 1 << 5, + + /// + /// The server supports the BINARYMIME extension. + /// + BinaryMime = 1 << 6, + + /// + /// The server supports the CHUNKING extension, + /// allowing clients to upload messages in chunks. + /// + Chunking = 1 << 7, + + /// + /// The server supports the STARTTLS extension, + /// allowing clients to switch to an encrypted SSL/TLS connection after connecting. + /// + StartTLS = 1 << 8, + + /// + /// The server supports the SMTPUTF8 extension. + /// + UTF8 = 1 << 9, + } +} diff --git a/src/MailKit/Net/Smtp/SmtpClient.cs b/src/MailKit/Net/Smtp/SmtpClient.cs new file mode 100644 index 0000000..c611f27 --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpClient.cs @@ -0,0 +1,2465 @@ +// +// SmtpClient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Linq; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +using MimeKit; +using MimeKit.IO; +using MimeKit.Cryptography; + +using MailKit.Security; + +using SslStream = MailKit.Net.SslStream; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Smtp { + /// + /// An SMTP client that can be used to send email messages. + /// + /// + /// The class supports both the "smtp" and "smtps" protocols. The "smtp" + /// protocol makes a clear-text connection to the SMTP server and does not use SSL or TLS unless the SMTP + /// server supports the STARTTLS extension. The "smtps" + /// protocol, however, connects to the SMTP server using an SSL-wrapped connection. + /// The connection established by any of the + /// Connect methods may be re-used if an + /// application wishes to send multiple messages to the same SMTP server. Since connecting and authenticating + /// can be expensive operations, re-using a connection can significantly improve performance when sending a + /// large number of messages to the same SMTP server over a short period of time. + /// + /// + /// + /// + public partial class SmtpClient : MailTransport, ISmtpClient + { + static readonly byte[] EndData = Encoding.ASCII.GetBytes (".\r\n"); + const int MaxLineLength = 998; + + enum SmtpCommand { + MailFrom, + RcptTo + } + + readonly HashSet authenticationMechanisms = new HashSet (StringComparer.Ordinal); + readonly List queued = new List (); + SmtpCapabilities capabilities; + int timeout = 2 * 60 * 1000; + bool authenticated; + bool connected; + bool disposed; + bool secure; + Uri uri; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can send messages with the , you must first call one of + /// the Connect methods. + /// Depending on whether the SMTP server requires authenticating or not, you may also need to + /// authenticate using one of the + /// Authenticate methods. + /// + /// + /// + /// + public SmtpClient () : this (new NullProtocolLogger ()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Before you can send messages with the , you must first call one of + /// the Connect methods. + /// Depending on whether the SMTP server requires authenticating or not, you may also need to + /// authenticate using one of the + /// Authenticate methods. + /// + /// The protocol logger. + /// + /// is null. + /// + /// + /// + /// + public SmtpClient (IProtocolLogger protocolLogger) : base (protocolLogger) + { + } + + /// + /// Get the underlying SMTP stream. + /// + /// + /// Gets the underlying SMTP stream. + /// + /// The SMTP stream. + SmtpStream Stream { + get; set; + } + + /// + /// Gets an object that can be used to synchronize access to the SMTP server. + /// + /// + /// Gets an object that can be used to synchronize access to the SMTP server between multiple threads. + /// When using methods from multiple threads, it is important to lock the + /// object for thread safety. + /// + /// The lock object. + public override object SyncRoot { + get { return this; } + } + + /// + /// Get the protocol supported by the message service. + /// + /// + /// Gets the protocol supported by the message service. + /// + /// The protocol. + protected override string Protocol { + get { return "smtp"; } + } + + /// + /// Get the capabilities supported by the SMTP server. + /// + /// + /// The capabilities will not be known until a successful connection has been made + /// and may change once the client is authenticated. + /// + /// + /// + /// + /// The capabilities. + /// + /// Capabilities cannot be enabled, they may only be disabled. + /// + public SmtpCapabilities Capabilities { + get { return capabilities; } + set { + if ((capabilities | value) > capabilities) + throw new ArgumentException ("Capabilities cannot be enabled, they may only be disabled.", nameof (value)); + + capabilities = value; + } + } + + /// + /// Get or set the local domain. + /// + /// + /// The local domain is used in the HELO or EHLO commands sent to + /// the SMTP server. If left unset, the local IP address will be + /// used instead. + /// + /// The local domain. + public string LocalDomain { + get; set; + } + + /// + /// Get whether or not the BDAT command is preferred over the DATA command. + /// + /// + /// Gets whether or not the BDAT command is preferred over the standard DATA + /// command. + /// The BDAT command is normally only used when the message being sent contains binary data + /// (e.g. one mor more MIME parts contains a Content-Transfer-Encoding: binary header). This + /// option provides a way to override this behavior, forcing the to send + /// messages using the BDAT command instead of the DATA command even when it is not + /// necessary to do so. + /// + /// true if the BDAT command is preferred over the DATA command; otherwise, false. + protected virtual bool PreferSendAsBinaryData { + get { return false; } + } + + /// + /// Get the maximum message size supported by the server. + /// + /// + /// The maximum message size will not be known until a successful connection has + /// been made and may change once the client is authenticated. + /// This value is only relevant if the includes + /// the flag. + /// + /// + /// + /// + /// The maximum message size supported by the server. + public uint MaxSize { + get; private set; + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (SmtpClient)); + } + + #region IMailService implementation + + /// + /// Get the authentication mechanisms supported by the SMTP server. + /// + /// + /// The authentication mechanisms are queried as part of the connection + /// process. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before authenticating. + /// + /// + /// + /// + /// The authentication mechanisms. + public override HashSet AuthenticationMechanisms { + get { return authenticationMechanisms; } + } + + /// + /// Get or set the timeout for network streaming operations, in milliseconds. + /// + /// + /// Gets or sets the underlying socket stream's + /// and values. + /// + /// The timeout in milliseconds. + public override int Timeout { + get { return timeout; } + set { + if (IsConnected && Stream.CanTimeout) { + Stream.WriteTimeout = value; + Stream.ReadTimeout = value; + } + + timeout = value; + } + } + + /// + /// Get whether or not the client is currently connected to an SMTP server. + /// + /// + /// The state is set to true immediately after + /// one of the Connect + /// methods succeeds and is not set back to false until either the client + /// is disconnected via or until an + /// is thrown while attempting to read or write to + /// the underlying network socket. + /// When an is caught, the connection state of the + /// should be checked before continuing. + /// + /// + /// + /// + /// true if the client is connected; otherwise, false. + public override bool IsConnected { + get { return connected; } + } + + /// + /// Get whether or not the connection is secure (typically via SSL or TLS). + /// + /// + /// Gets whether or not the connection is secure (typically via SSL or TLS). + /// + /// true if the connection is secure; otherwise, false. + public override bool IsSecure { + get { return IsConnected && secure; } + } + + /// + /// Get whether or not the client is currently authenticated with the SMTP server. + /// + /// + /// Gets whether or not the client is currently authenticated with the SMTP server. + /// To authenticate with the SMTP server, use one of the + /// Authenticate + /// methods. + /// + /// true if the client is connected; otherwise, false. + public override bool IsAuthenticated { + get { return authenticated; } + } + + bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (ServerCertificateValidationCallback != null) + return ServerCertificateValidationCallback (uri.Host, certificate, chain, sslPolicyErrors); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (ServicePointManager.ServerCertificateValidationCallback != null) + return ServicePointManager.ServerCertificateValidationCallback (uri.Host, certificate, chain, sslPolicyErrors); +#endif + + return DefaultServerCertificateValidationCallback (uri.Host, certificate, chain, sslPolicyErrors); + } + + async Task QueueCommandAsync (SmtpCommand type, string command, bool doAsync, CancellationToken cancellationToken) + { + var bytes = Encoding.UTF8.GetBytes (command + "\r\n"); + + // Note: queued commands will be buffered by the stream + if (doAsync) + await Stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); + else + Stream.Write (bytes, 0, bytes.Length, cancellationToken); + queued.Add (type); + } + + /// + /// Invoked only when no recipients were accepted by the SMTP server. + /// + /// + /// If is overridden to not throw + /// an exception, this method should be overridden to throw an appropriate + /// exception instead. + /// + /// The message being sent. + protected virtual void OnNoRecipientsAccepted (MimeMessage message) + { + } + + async Task FlushCommandQueueAsync (MimeMessage message, MailboxAddress sender, IList recipients, bool doAsync, CancellationToken cancellationToken) + { + try { + // Note: Queued commands are buffered by the stream + if (doAsync) + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + else + Stream.Flush (cancellationToken); + } catch { + queued.Clear (); + throw; + } + + var responses = new List (); + Exception rex = null; + int accepted = 0; + int rcpt = 0; + + // Note: We need to read all responses from the server before we can process + // them in case any of them have any errors so that we can RSET the state. + try { + for (int i = 0; i < queued.Count; i++) { + SmtpResponse response; + + if (doAsync) + response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + else + response = Stream.ReadResponse (cancellationToken); + + responses.Add (response); + } + } catch (Exception ex) { + // Note: Most likely this exception is due to an unexpected disconnect. + // Usually, before an SMTP server disconnects the client, it will send an + // error code response that will be more useful to the user than an error + // stating that the server has unexpected disconnected. Save this exception + // in case the server didn't give us a response with an error code. + rex = ex; + } + + try { + // process the responses + for (int i = 0; i < responses.Count; i++) { + switch (queued[i]) { + case SmtpCommand.MailFrom: + ProcessMailFromResponse (message, sender, responses[i]); + break; + case SmtpCommand.RcptTo: + if (ProcessRcptToResponse (message, recipients[rcpt++], responses[i])) + accepted++; + break; + } + } + } finally { + queued.Clear (); + } + + // throw the saved exception + if (rex != null) + throw rex; + + if (accepted == 0) + OnNoRecipientsAccepted (message); + } + + async Task SendCommandAsync (string command, bool doAsync, CancellationToken cancellationToken) + { + var bytes = Encoding.UTF8.GetBytes (command + "\r\n"); + + if (doAsync) { + await Stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + return await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + } + + Stream.Write (bytes, 0, bytes.Length, cancellationToken); + Stream.Flush (cancellationToken); + + return Stream.ReadResponse (cancellationToken); + } + + /// + /// Send a custom command to the SMTP server. + /// + /// + /// Sends a custom command to the SMTP server. + /// The command string should not include the terminating \r\n sequence. + /// + /// The command response. + /// The command. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + protected SmtpResponse SendCommand (string command, CancellationToken cancellationToken = default (CancellationToken)) + { + if (command == null) + throw new ArgumentNullException (nameof (command)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can send commands."); + + return SendCommandAsync (command, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task SendEhloAsync (bool ehlo, bool doAsync, CancellationToken cancellationToken) + { + var network = NetworkStream.Get (Stream.Stream); + string command = ehlo ? "EHLO " : "HELO "; + string domain = null; + IPAddress ip = null; + + if (!string.IsNullOrEmpty (LocalDomain)) { + if (!IPAddress.TryParse (LocalDomain, out ip)) + domain = LocalDomain; + } else if (network != null) { + var ipEndPoint = network.Socket.LocalEndPoint as IPEndPoint; + + if (ipEndPoint == null) + domain = ((DnsEndPoint) network.Socket.LocalEndPoint).Host; + else + ip = ipEndPoint.Address; + } else { + domain = "[127.0.0.1]"; + } + + if (ip != null) { + if (ip.IsIPv4MappedToIPv6) { + try { + ip = ip.MapToIPv4 (); + } catch (ArgumentOutOfRangeException) { + // .NET 4.5.2 bug on Windows 7 SP1 (issue #814) + } + } + + if (ip.AddressFamily == AddressFamily.InterNetworkV6) + domain = "[IPv6:" + ip + "]"; + else + domain = "[" + ip + "]"; + } + + command += domain; + + return await SendCommandAsync (command, doAsync, cancellationToken).ConfigureAwait (false); + } + + async Task EhloAsync (bool doAsync, CancellationToken cancellationToken) + { + SmtpResponse response; + + response = await SendEhloAsync (true, doAsync, cancellationToken).ConfigureAwait (false); + + // Some SMTP servers do not accept an EHLO after authentication (despite the rfc saying it is required). + if (authenticated && response.StatusCode == SmtpStatusCode.BadCommandSequence) + return; + + if (response.StatusCode != SmtpStatusCode.Ok) { + // Try sending HELO instead... + response = await SendEhloAsync (false, doAsync, cancellationToken).ConfigureAwait (false); + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } else { + // Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command. + capabilities &= SmtpCapabilities.StartTLS; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + var lines = response.Response.Split ('\n'); + for (int i = 0; i < lines.Length; i++) { + // Outlook.com replies with "250-8bitmime" instead of "250-8BITMIME" + // (strangely, it correctly capitalizes all other extensions...) + var capability = lines[i].Trim ().ToUpperInvariant (); + + if (capability.StartsWith ("AUTH", StringComparison.Ordinal) || capability.StartsWith ("X-EXPS", StringComparison.Ordinal)) { + int index = capability[0] == 'A' ? "AUTH".Length : "X-EXPS".Length; + + if (index < capability.Length && (capability[index] == ' ' || capability[index] == '=')) { + capabilities |= SmtpCapabilities.Authentication; + index++; + + var mechanisms = capability.Substring (index); + foreach (var mechanism in mechanisms.Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) + AuthenticationMechanisms.Add (mechanism); + } + } else if (capability.StartsWith ("SIZE", StringComparison.Ordinal)) { + int index = 4; + uint size; + + capabilities |= SmtpCapabilities.Size; + + while (index < capability.Length && char.IsWhiteSpace (capability[index])) + index++; + + if (uint.TryParse (capability.Substring (index), NumberStyles.None, CultureInfo.InvariantCulture, out size)) + MaxSize = size; + } else if (capability == "DSN") { + capabilities |= SmtpCapabilities.Dsn; + } else if (capability == "BINARYMIME") { + capabilities |= SmtpCapabilities.BinaryMime; + } else if (capability == "CHUNKING") { + capabilities |= SmtpCapabilities.Chunking; + } else if (capability == "ENHANCEDSTATUSCODES") { + capabilities |= SmtpCapabilities.EnhancedStatusCodes; + } else if (capability == "8BITMIME") { + capabilities |= SmtpCapabilities.EightBitMime; + } else if (capability == "PIPELINING") { + capabilities |= SmtpCapabilities.Pipelining; + } else if (capability == "STARTTLS") { + capabilities |= SmtpCapabilities.StartTLS; + } else if (capability == "SMTPUTF8") { + capabilities |= SmtpCapabilities.UTF8; + } + } + } + } + + async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, CancellationToken cancellationToken) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The SmtpClient is already authenticated."); + + if ((capabilities & SmtpCapabilities.Authentication) == 0) + throw new NotSupportedException ("The SMTP server does not support authentication."); + + cancellationToken.ThrowIfCancellationRequested (); + + SmtpResponse response; + string challenge; + string command; + + mechanism.Uri = new Uri ($"smtp://{uri.Host}"); + + // send an initial challenge if the mechanism supports it + if (mechanism.SupportsInitialResponse) { + challenge = mechanism.Challenge (null); + command = string.Format ("AUTH {0} {1}", mechanism.MechanismName, challenge); + } else { + command = string.Format ("AUTH {0}", mechanism.MechanismName); + } + + response = await SendCommandAsync (command, doAsync, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + throw new AuthenticationException (response.Response); + + SaslException saslException = null; + + try { + while (!mechanism.IsAuthenticated) { + if (response.StatusCode != SmtpStatusCode.AuthenticationChallenge) + break; + + challenge = mechanism.Challenge (response.Response); + response = await SendCommandAsync (challenge, doAsync, cancellationToken).ConfigureAwait (false); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = await SendCommandAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + saslException = ex; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { + if (mechanism.NegotiatedSecurityLayer) + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + authenticated = true; + OnAuthenticated (response.Response); + return; + } + + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); + + if (saslException != null) + throw new AuthenticationException (message, saslException); + + throw new AuthenticationException (message); + } + + /// + /// Authenticate using the specified SASL mechanism. + /// + /// + /// Authenticates using the specified SASL mechanism. + /// For a list of available SASL authentication mechanisms supported by the server, + /// check the property after the service has been + /// connected. + /// + /// The SASL mechanism. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (mechanism, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool doAsync, CancellationToken cancellationToken) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient must be connected before you can authenticate."); + + if (IsAuthenticated) + throw new InvalidOperationException ("The SmtpClient is already authenticated."); + + if ((capabilities & SmtpCapabilities.Authentication) == 0) + throw new NotSupportedException ("The SMTP server does not support authentication."); + + var saslUri = new Uri ($"smtp://{uri.Host}"); + AuthenticationException authException = null; + SmtpResponse response; + SaslMechanism sasl; + bool tried = false; + string challenge; + string command; + + foreach (var authmech in SaslMechanism.AuthMechanismRank) { + if (!AuthenticationMechanisms.Contains (authmech)) + continue; + + if ((sasl = SaslMechanism.Create (authmech, saslUri, encoding, credentials)) == null) + continue; + + tried = true; + + cancellationToken.ThrowIfCancellationRequested (); + + // send an initial challenge if the mechanism supports it + if (sasl.SupportsInitialResponse) { + challenge = sasl.Challenge (null); + command = string.Format ("AUTH {0} {1}", authmech, challenge); + } else { + command = string.Format ("AUTH {0}", authmech); + } + + response = await SendCommandAsync (command, doAsync, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode == SmtpStatusCode.AuthenticationMechanismTooWeak) + continue; + + SaslException saslException = null; + + try { + while (!sasl.IsAuthenticated) { + if (response.StatusCode != SmtpStatusCode.AuthenticationChallenge) + break; + + challenge = sasl.Challenge (response.Response); + response = await SendCommandAsync (challenge, doAsync, cancellationToken).ConfigureAwait (false); + } + + saslException = null; + } catch (SaslException ex) { + // reset the authentication state + response = await SendCommandAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + saslException = ex; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationSuccessful) { + if (sasl.NegotiatedSecurityLayer) + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + authenticated = true; + OnAuthenticated (response.Response); + return; + } + + var message = string.Format (CultureInfo.InvariantCulture, "{0}: {1}", (int) response.StatusCode, response.Response); + Exception inner; + + if (saslException != null) + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response, saslException); + else + inner = new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + authException = new AuthenticationException (message, inner); + } + + if (tried) + throw authException ?? new AuthenticationException (); + + throw new NotSupportedException ("No compatible authentication mechanisms found."); + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// If the SMTP server supports authentication, then the SASL mechanisms + /// that both the client and server support are tried in order of greatest + /// security to weakest security. Once a SASL authentication mechanism is + /// found that both client and server support, the credentials are used to + /// authenticate. + /// If, on the other hand, authentication is not supported by the SMTP + /// server, then this method will throw . + /// The property can be checked for the + /// flag to make sure the + /// SMTP server supports authentication before calling this method. + /// To prevent the usage of certain authentication mechanisms, + /// simply remove them from the hash set + /// before calling this method. + /// + /// The text encoding to use for the user's credentials. + /// The user's credentials. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The is not connected. + /// + /// + /// The is already authenticated. + /// + /// + /// The SMTP server does not support authentication. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// Authentication using the supplied credentials has failed. + /// + /// + /// A SASL authentication error occurred. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) + { + AuthenticateAsync (encoding, credentials, false, cancellationToken).GetAwaiter ().GetResult (); + } + + internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + Stream = new SmtpStream (replayStream, ProtocolLogger); + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + uri = new Uri ($"smtp://{host}:25"); + secure = false; + MaxSize = 0; + + try { + // read the greeting + var response = Stream.ReadResponse (cancellationToken); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + EhloAsync (false, cancellationToken).GetAwaiter ().GetResult (); + + connected = true; + } catch { + Stream.Dispose (); + Stream = null; + throw; + } + + OnConnected (host, 25, SecureSocketOptions.None); + } + + internal async Task ReplayConnectAsync (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) + { + CheckDisposed (); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (replayStream == null) + throw new ArgumentNullException (nameof (replayStream)); + + Stream = new SmtpStream (replayStream, ProtocolLogger); + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + uri = new Uri ($"smtp://{host}:25"); + secure = false; + MaxSize = 0; + + try { + // read the greeting + var response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + await EhloAsync (true, cancellationToken).ConfigureAwait (false); + + connected = true; + } catch { + Stream.Dispose (); + Stream = null; + throw; + } + + OnConnected (host, 25, SecureSocketOptions.None); + } + + internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) + { + switch (options) { + default: + if (port == 0) + port = 25; + break; + case SecureSocketOptions.Auto: + switch (port) { + case 0: port = 25; goto default; + case 465: options = SecureSocketOptions.SslOnConnect; break; + default: options = SecureSocketOptions.StartTlsWhenAvailable; break; + } + break; + case SecureSocketOptions.SslOnConnect: + if (port == 0) + port = 465; + break; + } + + switch (options) { + case SecureSocketOptions.StartTlsWhenAvailable: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}/?starttls=when-available", host, port)); + starttls = true; + break; + case SecureSocketOptions.StartTls: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}/?starttls=always", host, port)); + starttls = true; + break; + case SecureSocketOptions.SslOnConnect: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtps://{0}:{1}", host, port)); + starttls = false; + break; + default: + uri = new Uri (string.Format (CultureInfo.InvariantCulture, "smtp://{0}:{1}", host, port)); + starttls = false; + break; + } + } + + async Task ConnectAsync (string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The SmtpClient is already connected."); + + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + SmtpResponse response; + Stream stream; + bool starttls; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + var socket = await ConnectSocket (host, port, doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + secure = true; + stream = ssl; + } else { + stream = new NetworkStream (socket, true); + secure = false; + } + + if (stream.CanTimeout) { + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + } + + Stream = new SmtpStream (stream, ProtocolLogger); + + try { + ProtocolLogger.LogConnect (uri); + + // read the greeting + if (doAsync) + response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + else + response = Stream.ReadResponse (cancellationToken); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + + if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { + response = await SendCommandAsync ("STARTTLS", doAsync, cancellationToken).ConfigureAwait (false); + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + try { + var tls = new SslStream (stream, false, ValidateRemoteCertificate); + Stream.Stream = tls; + + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // Send EHLO again and get the new list of supported extensions + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + connected = true; + } catch { + Stream.Dispose (); + secure = false; + Stream = null; + throw; + } + + OnConnected (host, port, options); + } + + /// + /// Establish a connection to the specified SMTP or SMTP/S server. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server. + /// If the has a value of 0, then the + /// parameter is used to determine the default port to + /// connect to. The default port used with + /// is 465. All other values will use a default port of 25. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// The connection established by any of the + /// Connect + /// methods may be re-used if an application wishes to send multiple messages + /// to the same SMTP server. Since connecting and authenticating can be expensive + /// operations, re-using a connection can significantly improve performance when + /// sending a large number of messages to the same SMTP server over a short + /// period of time. + /// + /// + /// + /// + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// A socket error occurred trying to connect to the remote host. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (host == null) + throw new ArgumentNullException (nameof (host)); + + if (host.Length == 0) + throw new ArgumentException ("The host name cannot be empty.", nameof (host)); + + if (port < 0 || port > 65535) + throw new ArgumentOutOfRangeException (nameof (port)); + + CheckDisposed (); + + if (IsConnected) + throw new InvalidOperationException ("The SmtpClient is already connected."); + + capabilities = SmtpCapabilities.None; + AuthenticationMechanisms.Clear (); + MaxSize = 0; + + SmtpResponse response; + Stream network; + bool starttls; + + ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + if (doAsync) { + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (this, ex, false); + } + + network = ssl; + secure = true; + } else { + network = stream; + secure = false; + } + + if (network.CanTimeout) { + network.WriteTimeout = timeout; + network.ReadTimeout = timeout; + } + + Stream = new SmtpStream (network, ProtocolLogger); + + try { + ProtocolLogger.LogConnect (uri); + + // read the greeting + if (doAsync) + response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + else + response = Stream.ReadResponse (cancellationToken); + + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + // Send EHLO and get a list of supported extensions + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + + if (options == SecureSocketOptions.StartTls && (capabilities & SmtpCapabilities.StartTLS) == 0) + throw new NotSupportedException ("The SMTP server does not support the STARTTLS extension."); + + if (starttls && (capabilities & SmtpCapabilities.StartTLS) != 0) { + response = await SendCommandAsync ("STARTTLS", doAsync, cancellationToken).ConfigureAwait (false); + if (response.StatusCode != SmtpStatusCode.ServiceReady) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + var tls = new SslStream (network, false, ValidateRemoteCertificate); + Stream.Stream = tls; + + try { + if (doAsync) { + await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); + } else { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); +#else + tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); +#endif + } + } catch (Exception ex) { + throw SslHandshakeException.Create (this, ex, true); + } + + secure = true; + + // Send EHLO again and get the new list of supported extensions + await EhloAsync (doAsync, cancellationToken).ConfigureAwait (false); + } + + connected = true; + } catch { + Stream.Dispose (); + secure = false; + Stream = null; + throw; + } + + OnConnected (host, port, options); + } + + Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + { + if (socket == null) + throw new ArgumentNullException (nameof (socket)); + + if (!socket.Connected) + throw new ArgumentException ("The socket is not connected.", nameof (socket)); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, doAsync, cancellationToken); + } + + /// + /// Establish a connection to the specified SMTP or SMTP/S server using the provided socket. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided socket. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The socket to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// is not connected. + /// -or- + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (socket, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Establish a connection to the specified SMTP or SMTP/S server using the provided stream. + /// + /// + /// Establishes a connection to the specified SMTP or SMTP/S server using the provided stream. + /// If the has a value of + /// , then the is used + /// to determine the default security options. If the has a value + /// of 465, then the default options used will be + /// . All other values will use + /// . + /// Once a connection is established, properties such as + /// and will be + /// populated. + /// With the exception of using the to determine the + /// default to use when the value + /// is , the and + /// parameters are only used for logging purposes. + /// + /// The stream to use for the connection. + /// The host name to connect to. + /// The port to connect to. If the specified port is 0, then the default port will be used. + /// The secure socket options to when connecting. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not between 0 and 65535. + /// + /// + /// The is a zero-length string. + /// + /// + /// The has been disposed. + /// + /// + /// The is already connected. + /// + /// + /// was set to + /// + /// and the SMTP server does not support the STARTTLS extension. + /// + /// + /// The operation was canceled. + /// + /// + /// An error occurred during the SSL/TLS negotiations. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) + { + ConnectAsync (stream, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task DisconnectAsync (bool quit, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (!IsConnected) + return; + + if (quit) { + try { + await SendCommandAsync ("QUIT", doAsync, cancellationToken).ConfigureAwait (false); + } catch (OperationCanceledException) { + } catch (SmtpProtocolException) { + } catch (SmtpCommandException) { + } catch (IOException) { + } + } + + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), true); + } + + /// + /// Disconnect the service. + /// + /// + /// If is true, a QUIT command will be issued in order to disconnect cleanly. + /// + /// + /// + /// + /// If set to true, a QUIT command will be issued in order to disconnect cleanly. + /// The cancellation token. + /// + /// The has been disposed. + /// + public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) + { + DisconnectAsync (quit, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task NoOpAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient is not connected."); + + SmtpResponse response; + + try { + response = await SendCommandAsync ("NOOP", doAsync, cancellationToken).ConfigureAwait (false); + } catch { + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), false); + throw; + } + + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } + + /// + /// Ping the SMTP server to keep the connection alive. + /// + /// Mail servers, if left idle for too long, will automatically drop the connection. + /// The cancellation token. + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// The operation was canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol error occurred. + /// + public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) + { + NoOpAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + void Disconnect (string host, int port, SecureSocketOptions options, bool requested) + { + capabilities = SmtpCapabilities.None; + authenticated = false; + connected = false; + secure = false; + uri = null; + + if (Stream != null) { + Stream.Dispose (); + Stream = null; + } + + if (host != null) + OnDisconnected (host, port, options, requested); + } + + #endregion + + #region IMailTransport implementation + + static MailboxAddress GetMessageSender (MimeMessage message) + { + if (message.ResentSender != null) + return message.ResentSender; + + if (message.ResentFrom.Count > 0) + return message.ResentFrom.Mailboxes.FirstOrDefault (); + + if (message.Sender != null) + return message.Sender; + + return message.From.Mailboxes.FirstOrDefault (); + } + + static void AddUnique (IList recipients, HashSet unique, IEnumerable mailboxes) + { + foreach (var mailbox in mailboxes) { + if (unique.Add (mailbox.Address)) + recipients.Add (mailbox); + } + } + + static IList GetMessageRecipients (MimeMessage message) + { + var unique = new HashSet (StringComparer.OrdinalIgnoreCase); + var recipients = new List (); + + if (message.ResentSender != null || message.ResentFrom.Count > 0) { + AddUnique (recipients, unique, message.ResentTo.Mailboxes); + AddUnique (recipients, unique, message.ResentCc.Mailboxes); + AddUnique (recipients, unique, message.ResentBcc.Mailboxes); + } else { + AddUnique (recipients, unique, message.To.Mailboxes); + AddUnique (recipients, unique, message.Cc.Mailboxes); + AddUnique (recipients, unique, message.Bcc.Mailboxes); + } + + return recipients; + } + + [Flags] + enum SmtpExtension { + None = 0, + EightBitMime = 1 << 0, + BinaryMime = 1 << 1, + UTF8 = 1 << 2, + } + + class ContentTransferEncodingVisitor : MimeVisitor + { + readonly SmtpCapabilities capabilities; + + public ContentTransferEncodingVisitor (SmtpCapabilities capabilities) + { + this.capabilities = capabilities; + } + + public SmtpExtension SmtpExtensions { + get; private set; + } + + protected override void VisitMimePart (MimePart entity) + { + switch (entity.ContentTransferEncoding) { + case ContentEncoding.EightBit: + if ((capabilities & SmtpCapabilities.EightBitMime) != 0) + SmtpExtensions |= SmtpExtension.EightBitMime; + break; + case ContentEncoding.Binary: + if ((capabilities & SmtpCapabilities.BinaryMime) != 0) + SmtpExtensions |= SmtpExtension.BinaryMime; + break; + } + } + } + + /// + /// Invoked when the sender is accepted by the SMTP server. + /// + /// + /// The default implementation does nothing. + /// + /// The message being sent. + /// The mailbox used in the MAIL FROM command. + /// The response to the MAIL FROM command. + protected virtual void OnSenderAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + } + + /// + /// Invoked when a recipient is not accepted by the SMTP server. + /// + /// + /// The default implementation throws an appropriate . + /// + /// The message being sent. + /// The mailbox used in the MAIL FROM command. + /// The response to the MAIL FROM command. + protected virtual void OnSenderNotAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + throw new SmtpCommandException (SmtpErrorCode.SenderNotAccepted, response.StatusCode, mailbox, response.Response); + } + + void ProcessMailFromResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + if (response.StatusCode >= SmtpStatusCode.Ok && response.StatusCode < (SmtpStatusCode) 260) { + OnSenderAccepted (message, mailbox, response); + return; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationRequired) + throw new ServiceNotAuthenticatedException (response.Response); + + OnSenderNotAccepted (message, mailbox, response); + } + + /// + /// Get the envelope identifier to be used with delivery status notifications. + /// + /// + /// The envelope identifier, if non-empty, is useful in determining which message a delivery + /// status notification was issued for. + /// The envelope identifier should be unique and may be up to 100 characters in length, but + /// must consist only of printable ASCII characters and no white space. + /// For more information, see + /// rfc3461, section 4.4. + /// + /// + /// + /// + /// The envelope identifier. + /// The message. + protected virtual string GetEnvelopeId (MimeMessage message) + { + return null; + } + + /// + /// Get or set how much of the message to include in any failed delivery status notifications. + /// + /// + /// Gets or sets how much of the message to include in any failed delivery status notifications. + /// + /// A value indicating how much of the message to include in a failure delivery status notification. + public DeliveryStatusNotificationType DeliveryStatusNotificationType { + get; set; + } + + async Task MailFromAsync (FormatOptions options, MimeMessage message, MailboxAddress mailbox, SmtpExtension extensions, long size, bool doAsync, CancellationToken cancellationToken) + { + var idnEncode = (extensions & SmtpExtension.UTF8) == 0; + var builder = new StringBuilder ("MAIL FROM:<"); + + var addrspec = mailbox.GetAddress (idnEncode); + builder.Append (addrspec); + builder.Append ('>'); + + if (!idnEncode) + builder.Append (" SMTPUTF8"); + + if ((Capabilities & SmtpCapabilities.Size) != 0 && size != -1) + builder.AppendFormat (CultureInfo.InvariantCulture, " SIZE={0}", size); + + if ((extensions & SmtpExtension.BinaryMime) != 0) + builder.Append (" BODY=BINARYMIME"); + else if ((extensions & SmtpExtension.EightBitMime) != 0) + builder.Append (" BODY=8BITMIME"); + + if ((capabilities & SmtpCapabilities.Dsn) != 0) { + var envid = GetEnvelopeId (message); + + if (!string.IsNullOrEmpty (envid)) { + builder.Append (" ENVID="); + builder.Append (envid); + } + + switch (DeliveryStatusNotificationType) { + case DeliveryStatusNotificationType.HeadersOnly: + builder.Append (" RET=HDRS"); + break; + case DeliveryStatusNotificationType.Full: + builder.Append (" RET=FULL"); + break; + } + } + + var command = builder.ToString (); + + if ((capabilities & SmtpCapabilities.Pipelining) != 0) { + await QueueCommandAsync (SmtpCommand.MailFrom, command, doAsync, cancellationToken).ConfigureAwait (false); + return; + } + + var response = await SendCommandAsync (command, doAsync, cancellationToken).ConfigureAwait (false); + + ProcessMailFromResponse (message, mailbox, response); + } + + /// + /// Invoked when a recipient is accepted by the SMTP server. + /// + /// + /// The default implementation does nothing. + /// + /// The message being sent. + /// The mailbox used in the RCPT TO command. + /// The response to the RCPT TO command. + protected virtual void OnRecipientAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + } + + /// + /// Invoked when a recipient is not accepted by the SMTP server. + /// + /// + /// The default implementation throws an appropriate . + /// + /// The message being sent. + /// The mailbox used in the RCPT TO command. + /// The response to the RCPT TO command. + protected virtual void OnRecipientNotAccepted (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + throw new SmtpCommandException (SmtpErrorCode.RecipientNotAccepted, response.StatusCode, mailbox, response.Response); + } + + bool ProcessRcptToResponse (MimeMessage message, MailboxAddress mailbox, SmtpResponse response) + { + if (response.StatusCode < (SmtpStatusCode) 300) { + OnRecipientAccepted (message, mailbox, response); + return true; + } + + if (response.StatusCode == SmtpStatusCode.AuthenticationRequired) + throw new ServiceNotAuthenticatedException (response.Response); + + OnRecipientNotAccepted (message, mailbox, response); + + return false; + } + + /// + /// Get the types of delivery status notification desired for the specified recipient mailbox. + /// + /// + /// Gets the types of delivery status notification desired for the specified recipient mailbox. + /// + /// + /// + /// + /// The desired delivery status notification type. + /// The message being sent. + /// The mailbox. + protected virtual DeliveryStatusNotification? GetDeliveryStatusNotifications (MimeMessage message, MailboxAddress mailbox) + { + return null; + } + + static string GetNotifyString (DeliveryStatusNotification notify) + { + string value = string.Empty; + + if (notify == DeliveryStatusNotification.Never) + return "NEVER"; + + if ((notify & DeliveryStatusNotification.Success) != 0) + value += "SUCCESS,"; + + if ((notify & DeliveryStatusNotification.Failure) != 0) + value += "FAILURE,"; + + if ((notify & DeliveryStatusNotification.Delay) != 0) + value += "DELAY"; + + return value.TrimEnd (','); + } + + async Task RcptToAsync (FormatOptions options, MimeMessage message, MailboxAddress mailbox, bool doAsync, CancellationToken cancellationToken) + { + var idnEncode = (Capabilities & SmtpCapabilities.UTF8) == 0; + var command = string.Format ("RCPT TO:<{0}>", mailbox.GetAddress (idnEncode)); + + if ((capabilities & SmtpCapabilities.Dsn) != 0) { + var notify = GetDeliveryStatusNotifications (message, mailbox); + + if (notify.HasValue) + command += " NOTIFY=" + GetNotifyString (notify.Value); + } + + if ((capabilities & SmtpCapabilities.Pipelining) != 0) { + await QueueCommandAsync (SmtpCommand.RcptTo, command, doAsync, cancellationToken).ConfigureAwait (false); + return false; + } + + var response = await SendCommandAsync (command, doAsync, cancellationToken).ConfigureAwait (false); + + return ProcessRcptToResponse (message, mailbox, response); + } + + class SendContext + { + readonly ITransferProgress progress; + readonly long size; + long nwritten; + + public SendContext (ITransferProgress progress, long size) + { + this.progress = progress; + this.size = size; + } + + public void Update (int n) + { + nwritten += n; + + if (size != -1) + progress.Report (nwritten, size); + else + progress.Report (nwritten); + } + } + + async Task BdatAsync (FormatOptions options, MimeMessage message, long size, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + SmtpResponse response; + byte[] bytes; + + bytes = Encoding.UTF8.GetBytes (string.Format (CultureInfo.InvariantCulture, "BDAT {0} LAST\r\n", size)); + + if (doAsync) + await Stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); + else + Stream.Write (bytes, 0, bytes.Length, cancellationToken); + + if (progress != null) { + var ctx = new SendContext (progress, size); + + using (var stream = new ProgressStream (Stream, ctx.Update)) { + if (doAsync) { + await message.WriteToAsync (options, stream, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + message.WriteTo (options, stream, cancellationToken); + stream.Flush (cancellationToken); + } + } + } else if (doAsync) { + await message.WriteToAsync (options, Stream, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + message.WriteTo (options, Stream, cancellationToken); + Stream.Flush (cancellationToken); + } + + if (doAsync) + response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + else + response = Stream.ReadResponse (cancellationToken); + + switch (response.StatusCode) { + default: + throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, response.StatusCode, response.Response); + case SmtpStatusCode.AuthenticationRequired: + throw new ServiceNotAuthenticatedException (response.Response); + case SmtpStatusCode.Ok: + OnMessageSent (new MessageSentEventArgs (message, response.Response)); + break; + } + } + + async Task DataAsync (FormatOptions options, MimeMessage message, long size, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + var response = await SendCommandAsync ("DATA", doAsync, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.StartMailInput) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + if (progress != null) { + var ctx = new SendContext (progress, size); + + using (var stream = new ProgressStream (Stream, ctx.Update)) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new SmtpDataFilter ()); + + if (doAsync) { + await message.WriteToAsync (options, filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + message.WriteTo (options, filtered, cancellationToken); + filtered.Flush (cancellationToken); + } + } + } + } else { + using (var filtered = new FilteredStream (Stream)) { + filtered.Add (new SmtpDataFilter ()); + + if (doAsync) { + await message.WriteToAsync (options, filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + message.WriteTo (options, filtered, cancellationToken); + filtered.Flush (cancellationToken); + } + } + } + + if (doAsync) { + await Stream.WriteAsync (EndData, 0, EndData.Length, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + + response = await Stream.ReadResponseAsync (cancellationToken).ConfigureAwait (false); + } else { + Stream.Write (EndData, 0, EndData.Length, cancellationToken); + Stream.Flush (cancellationToken); + + response = Stream.ReadResponse (cancellationToken); + } + + switch (response.StatusCode) { + default: + throw new SmtpCommandException (SmtpErrorCode.MessageNotAccepted, response.StatusCode, response.Response); + case SmtpStatusCode.AuthenticationRequired: + throw new ServiceNotAuthenticatedException (response.Response); + case SmtpStatusCode.Ok: + OnMessageSent (new MessageSentEventArgs (message, response.Response)); + break; + } + } + + async Task ResetAsync (bool doAsync, CancellationToken cancellationToken) + { + try { + var response = await SendCommandAsync ("RSET", doAsync, cancellationToken).ConfigureAwait (false); + if (response.StatusCode != SmtpStatusCode.Ok) + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), false); + } catch (SmtpCommandException) { + // do not disconnect + } catch { + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri), false); + } + } + + /// + /// Prepare the message for transport with the specified constraints. + /// + /// + /// Prepares the message for transport with the specified constraints. + /// Typically, this involves calling on + /// the message with the provided constraints. + /// + /// The format options. + /// The message. + /// The encoding constraint. + /// The max line length supported by the server. + protected virtual void Prepare (FormatOptions options, MimeMessage message, EncodingConstraint constraint, int maxLineLength) + { + if (!message.Headers.Contains (HeaderId.DomainKeySignature) && + !message.Headers.Contains (HeaderId.DkimSignature) && + !message.Headers.Contains (HeaderId.ArcSeal)) { + // prepare the message + message.Prepare (constraint, maxLineLength); + } else { + // Note: we do not want to risk reformatting of headers to the international + // UTF-8 encoding, so disable it. + options.International = false; + } + } + + static async Task GetSizeAsync (FormatOptions options, MimeMessage message, bool doAsync, CancellationToken cancellationToken) + { + using (var measure = new MeasuringStream ()) { + if (doAsync) + await message.WriteToAsync (options, measure, cancellationToken).ConfigureAwait (false); + else + message.WriteTo (options, measure, cancellationToken); + + return measure.Length; + } + } + + /// + /// Get the size of the message. + /// + /// + /// Calculates the size of the message in bytes. + /// This method is called by Send + /// methods in the following conditions: + /// + /// The SMTP server supports the SIZE= parameter in the MAIL FROM command. + /// The parameter is non-null. + /// The SMTP server supports the CHUNKING extension. + /// + /// + /// The size of the message, in bytes. + /// The formatting options. + /// The message. + /// The cancellation token. + protected virtual long GetSize (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + return GetSizeAsync (options, message, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously get the size of the message. + /// + /// + /// Asynchronously calculates the size of the message in bytes. + /// This method is called by SendAsync + /// methods in the following conditions: + /// + /// The SMTP server supports the SIZE= parameter in the MAIL FROM command. + /// The parameter is non-null. + /// The SMTP server supports the CHUNKING extension. + /// + /// + /// The size of the message, in bytes. + /// The formatting options. + /// The message. + /// The cancellation token. + protected virtual Task GetSizeAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken) + { + return GetSizeAsync (options, message, true, cancellationToken); + } + + async Task SendAsync (FormatOptions options, MimeMessage message, MailboxAddress sender, IList recipients, bool doAsync, CancellationToken cancellationToken, ITransferProgress progress) + { + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient is not connected."); + + var format = options.Clone (); + format.NewLineFormat = NewLineFormat.Dos; + format.EnsureNewLine = true; + + if (format.International && (Capabilities & SmtpCapabilities.UTF8) == 0) + format.International = false; + + if (format.International && (Capabilities & SmtpCapabilities.EightBitMime) == 0) + throw new NotSupportedException ("The SMTP server does not support the 8BITMIME extension."); + + EncodingConstraint constraint; + long size; + + if ((Capabilities & SmtpCapabilities.BinaryMime) != 0) + constraint = EncodingConstraint.None; + else if ((Capabilities & SmtpCapabilities.EightBitMime) != 0) + constraint = EncodingConstraint.EightBit; + else + constraint = EncodingConstraint.SevenBit; + + Prepare (format, message, constraint, MaxLineLength); + + // figure out which SMTP extensions we need to use + var visitor = new ContentTransferEncodingVisitor (capabilities); + visitor.Visit (message); + + var extensions = visitor.SmtpExtensions; + + if ((Capabilities & SmtpCapabilities.UTF8) != 0 && (format.International || sender.IsInternational || recipients.Any (x => x.IsInternational))) + extensions |= SmtpExtension.UTF8; + + if ((Capabilities & (SmtpCapabilities.Chunking | SmtpCapabilities.Size)) != 0 || progress != null) { + if (doAsync) + size = await GetSizeAsync (format, message, cancellationToken); + else + size = GetSize (format, message, cancellationToken); + } else { + size = -1; + } + + try { + // Note: if PIPELINING is supported, MailFrom() and RcptTo() will + // queue their commands instead of sending them immediately. + await MailFromAsync (format, message, sender, extensions, size, doAsync, cancellationToken).ConfigureAwait (false); + + int accepted = 0; + for (int i = 0; i < recipients.Count; i++) { + if (await RcptToAsync (format, message, recipients[i], doAsync, cancellationToken).ConfigureAwait (false)) + accepted++; + } + + if (queued.Count > 0) { + // Note: if PIPELINING is supported, this will flush all outstanding + // MAIL FROM and RCPT TO commands to the server and then process all + // of their responses. + await FlushCommandQueueAsync (message, sender, recipients, doAsync, cancellationToken).ConfigureAwait (false); + } else if (accepted == 0) { + OnNoRecipientsAccepted (message); + } + + if ((extensions & SmtpExtension.BinaryMime) != 0 || (PreferSendAsBinaryData && (Capabilities & SmtpCapabilities.BinaryMime) != 0)) + await BdatAsync (format, message, size, doAsync, cancellationToken, progress).ConfigureAwait (false); + else + await DataAsync (format, message, size, doAsync, cancellationToken, progress).ConfigureAwait (false); + } catch (ServiceNotAuthenticatedException) { + // do not disconnect + throw; + } catch (SmtpCommandException) { + await ResetAsync (doAsync, cancellationToken).ConfigureAwait (false); + throw; + } catch { + Disconnect (uri.Host, uri.Port, GetSecureSocketOptions (uri) , false); + throw; + } + } + + /// + /// Send the specified message. + /// + /// + /// Sends the specified message. + /// The sender address is determined by checking the following + /// message headers (in order of precedence): Resent-Sender, + /// Resent-From, Sender, and From. + /// If either the Resent-Sender or Resent-From addresses are present, + /// the recipients are collected from the Resent-To, Resent-Cc, and + /// Resent-Bcc headers, otherwise the To, Cc, and Bcc headers are used. + /// + /// + /// + /// + /// The formatting options. + /// The message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override void Send (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + var recipients = GetMessageRecipients (message); + var sender = GetMessageSender (message); + + if (sender == null) + throw new InvalidOperationException ("No sender has been specified."); + + if (recipients.Count == 0) + throw new InvalidOperationException ("No recipients have been specified."); + + SendAsync (options, message, sender, recipients, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + /// + /// Send the specified message using the supplied sender and recipients. + /// + /// + /// Sends the message by uploading it to an SMTP server using the supplied sender and recipients. + /// + /// The formatting options. + /// The message. + /// The mailbox address to use for sending the message. + /// The mailbox addresses that should receive the message. + /// The cancellation token. + /// The progress reporting mechanism. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before sending a message. + /// + /// + /// A sender has not been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// Internationalized formatting was requested but is not supported by the server. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public override void Send (FormatOptions options, MimeMessage message, MailboxAddress sender, IEnumerable recipients, CancellationToken cancellationToken = default (CancellationToken), ITransferProgress progress = null) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (sender == null) + throw new ArgumentNullException (nameof (sender)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + var unique = new HashSet (StringComparer.OrdinalIgnoreCase); + var rcpts = new List (); + + AddUnique (rcpts, unique, recipients); + + if (rcpts.Count == 0) + throw new InvalidOperationException ("No recipients have been specified."); + + SendAsync (options, message, sender, rcpts, false, cancellationToken, progress).GetAwaiter ().GetResult (); + } + + #endregion + + async Task ExpandAsync (string alias, bool doAsync, CancellationToken cancellationToken) + { + if (alias == null) + throw new ArgumentNullException (nameof (alias)); + + if (alias.Length == 0) + throw new ArgumentException ("The alias cannot be empty.", nameof (alias)); + + if (alias.IndexOfAny (new [] { '\r', '\n' }) != -1) + throw new ArgumentException ("The alias cannot contain newline characters.", nameof (alias)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient is not connected."); + + var response = await SendCommandAsync (string.Format ("EXPN {0}", alias), doAsync, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode != SmtpStatusCode.Ok) + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + + var lines = response.Response.Split ('\n'); + var list = new InternetAddressList (); + + for (int i = 0; i < lines.Length; i++) { + InternetAddress address; + + if (InternetAddress.TryParse (lines[i], out address)) + list.Add (address); + } + + return list; + } + + /// + /// Expand a mailing address alias. + /// + /// + /// Expands a mailing address alias. + /// + /// The expanded list of mailbox addresses. + /// The mailing address alias. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public InternetAddressList Expand (string alias, CancellationToken cancellationToken = default (CancellationToken)) + { + return ExpandAsync (alias, false, cancellationToken).GetAwaiter ().GetResult (); + } + + async Task VerifyAsync (string address, bool doAsync, CancellationToken cancellationToken) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + if (address.Length == 0) + throw new ArgumentException ("The address cannot be empty.", nameof (address)); + + if (address.IndexOfAny (new [] { '\r', '\n' }) != -1) + throw new ArgumentException ("The address cannot contain newline characters.", nameof (address)); + + CheckDisposed (); + + if (!IsConnected) + throw new ServiceNotConnectedException ("The SmtpClient is not connected."); + + var response = await SendCommandAsync (string.Format ("VRFY {0}", address), doAsync, cancellationToken).ConfigureAwait (false); + + if (response.StatusCode == SmtpStatusCode.Ok) + return MailboxAddress.Parse (response.Response); + + throw new SmtpCommandException (SmtpErrorCode.UnexpectedStatusCode, response.StatusCode, response.Response); + } + + /// + /// Verify the existence of a mailbox address. + /// + /// + /// Verifies the existence a mailbox address with the SMTP server, returning the expanded + /// mailbox address if it exists. + /// + /// The expanded mailbox address. + /// The mailbox address. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is an empty string. + /// + /// + /// The has been disposed. + /// + /// + /// The is not connected. + /// + /// + /// Authentication is required before verifying the existence of an address. + /// + /// + /// The operation has been canceled. + /// + /// + /// An I/O error occurred. + /// + /// + /// The SMTP command failed. + /// + /// + /// An SMTP protocol exception occurred. + /// + public MailboxAddress Verify (string address, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (address, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + disposed = true; + Disconnect (null, 0, SecureSocketOptions.None, false); + } + + base.Dispose (disposed); + } + } +} diff --git a/src/MailKit/Net/Smtp/SmtpCommandException.cs b/src/MailKit/Net/Smtp/SmtpCommandException.cs new file mode 100644 index 0000000..f2f0460 --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpCommandException.cs @@ -0,0 +1,258 @@ +// +// SmtpCommandException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +using MimeKit; + +namespace MailKit.Net.Smtp { + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + /// + /// + /// + public enum SmtpErrorCode { + /// + /// The message was not accepted for delivery. This may happen if + /// the server runs out of available disk space. + /// + MessageNotAccepted, + + /// + /// The sender's mailbox address was not accepted. Check the + /// property for the + /// mailbox used as the sender's mailbox address. + /// + SenderNotAccepted, + + /// + /// A recipient's mailbox address was not accepted. Check the + /// property for the + /// particular recipient mailbox that was not acccepted. + /// + RecipientNotAccepted, + + /// + /// An unexpected status code was returned by the server. + /// For more details, the + /// property may provide some additional hints. + /// + UnexpectedStatusCode, + } + + /// + /// An SMTP protocol exception. + /// + /// + /// The exception that is thrown when an SMTP command fails. Unlike a , + /// a does not require the to be reconnected. + /// + /// + /// + /// +#if SERIALIZABLE + [Serializable] +#endif + public class SmtpCommandException : CommandException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected SmtpCommandException (SerializationInfo info, StreamingContext context) : base (info, context) + { + MailboxAddress mailbox; + string value; + + value = info.GetString ("Mailbox"); + if (!string.IsNullOrEmpty (value) && MailboxAddress.TryParse (value, out mailbox)) + Mailbox = mailbox; + + ErrorCode = (SmtpErrorCode) info.GetValue ("ErrorCode", typeof (SmtpErrorCode)); + StatusCode = (SmtpStatusCode) info.GetValue ("StatusCode", typeof (SmtpStatusCode)); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code. + /// The rejected mailbox. + /// The error message. + /// The inner exception. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, MailboxAddress mailbox, string message, Exception innerException) : base (message, innerException) + { + StatusCode = status; + Mailbox = mailbox; + ErrorCode = code; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code. + /// The rejected mailbox. + /// The error message. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, MailboxAddress mailbox, string message) : base (message) + { + StatusCode = status; + Mailbox = mailbox; + ErrorCode = code; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code.> + /// The error message. + /// The inner exception. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, string message, Exception innerException) : base (message, innerException) + { + StatusCode = status; + ErrorCode = code; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error code. + /// The status code.> + /// The error message. + public SmtpCommandException (SmtpErrorCode code, SmtpStatusCode status, string message) : base (message) + { + StatusCode = status; + ErrorCode = code; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + if (Mailbox != null) + info.AddValue ("Mailbox", Mailbox.ToString ()); + else + info.AddValue ("Mailbox", string.Empty); + + info.AddValue ("ErrorCode", ErrorCode, typeof (SmtpErrorCode)); + info.AddValue ("StatusCode", StatusCode, typeof (SmtpStatusCode)); + } +#endif + + /// + /// Gets the error code which may provide additional information. + /// + /// + /// The error code can be used to programatically deal with the + /// exception without necessarily needing to display the raw + /// exception message to the user. + /// + /// + /// + /// + /// The status code. + public SmtpErrorCode ErrorCode { + get; private set; + } + + /// + /// Gets the mailbox that the error occurred on. + /// + /// + /// This property will only be available when the + /// value is either or + /// and may be used + /// to help the user decide how to proceed. + /// + /// + /// + /// + /// The mailbox. + public MailboxAddress Mailbox { + get; private set; + } + + /// + /// Gets the status code returned by the SMTP server. + /// + /// + /// The raw SMTP status code that resulted in the + /// being thrown. + /// + /// + /// + /// + /// The status code. + public SmtpStatusCode StatusCode { + get; private set; + } + } +} diff --git a/src/MailKit/Net/Smtp/SmtpDataFilter.cs b/src/MailKit/Net/Smtp/SmtpDataFilter.cs new file mode 100644 index 0000000..a5e81d6 --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpDataFilter.cs @@ -0,0 +1,140 @@ +// +// SmtpDataFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.IO.Filters; + +namespace MailKit.Net.Smtp { + /// + /// An SMTP filter designed to format a message stream for the DATA command. + /// + /// + /// A special stream filter that escapes lines beginning with a '.' as needed when + /// sending a message via the SMTP protocol or when saving a message to an IIS + /// message pickup directory. + /// + /// + /// + /// + public class SmtpDataFilter : MimeFilterBase + { + bool bol; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + public SmtpDataFilter () + { + bol = true; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int inputEnd = startIndex + length; + bool escape = bol; + int ndots = 0; + int crlf = 0; + + for (int i = startIndex; i < inputEnd; i++) { + byte c = input[i]; + + if (c == (byte) '.' && escape) { + escape = false; + ndots++; + } else { + escape = c == (byte) '\n'; + } + } + + if (flush && !escape) + crlf = 2; + + if (ndots + crlf == 0) { + outputIndex = startIndex; + outputLength = length; + bol = escape; + return input; + } + + EnsureOutputSize (length + ndots + crlf, false); + int index = 0; + + for (int i = startIndex; i < inputEnd; i++) { + byte c = input[i]; + + if (c == (byte) '.' && bol) { + OutputBuffer[index++] = (byte) '.'; + bol = false; + } else { + bol = c == (byte) '\n'; + } + + OutputBuffer[index++] = c; + } + + if (crlf > 0) { + OutputBuffer[index++] = (byte) '\r'; + OutputBuffer[index++] = (byte) '\n'; + } + + outputLength = index; + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Reset the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + base.Reset (); + bol = true; + } + } +} diff --git a/src/MailKit/Net/Smtp/SmtpProtocolException.cs b/src/MailKit/Net/Smtp/SmtpProtocolException.cs new file mode 100644 index 0000000..aa973cd --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpProtocolException.cs @@ -0,0 +1,101 @@ +// +// SmtpProtocolException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit.Net.Smtp { + /// + /// An SMTP protocol exception. + /// + /// + /// The exception that is thrown when there is an error communicating with an SMTP server. An + /// is typically fatal and requires the + /// to be reconnected. + /// + /// + /// + /// +#if SERIALIZABLE + [Serializable] +#endif + public class SmtpProtocolException : ProtocolException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the serialized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected SmtpProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The inner exception. + public SmtpProtocolException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public SmtpProtocolException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public SmtpProtocolException () + { + } + } +} diff --git a/src/MailKit/Net/Smtp/SmtpResponse.cs b/src/MailKit/Net/Smtp/SmtpResponse.cs new file mode 100644 index 0000000..dd1c598 --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpResponse.cs @@ -0,0 +1,68 @@ +// +// SmtpResponse.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Smtp { + /// + /// An SMTP command response. + /// + /// + /// An SMTP command response. + /// + public class SmtpResponse + { + /// + /// Get the status code. + /// + /// + /// Gets the status code. + /// + /// The status code. + public SmtpStatusCode StatusCode { get; private set; } + + /// + /// Get the response text. + /// + /// + /// Gets the response text. + /// + /// The response text. + public string Response { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The status code. + /// The response text. + public SmtpResponse (SmtpStatusCode code, string response) + { + StatusCode = code; + Response = response; + } + } +} diff --git a/src/MailKit/Net/Smtp/SmtpStatusCode.cs b/src/MailKit/Net/Smtp/SmtpStatusCode.cs new file mode 100644 index 0000000..b97cc2d --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpStatusCode.cs @@ -0,0 +1,190 @@ +// +// SmtpStatusCode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Net.Smtp { + /// + /// An enumeration of possible SMTP status codes. + /// + /// + /// An enumeration of possible SMTP status codes. + /// + public enum SmtpStatusCode { + /// + /// The "system status" status code. + /// + SystemStatus = 211, + + /// + /// The "help" status code. + /// + HelpMessage = 214, + + /// + /// The "service ready" status code. + /// + ServiceReady = 220, + + /// + /// The "service closing transmission channel" status code. + /// + ServiceClosingTransmissionChannel = 221, + + /// + /// The "authentication successful" status code. + /// + AuthenticationSuccessful = 235, + + /// + /// The general purpose "OK" status code. + /// + Ok = 250, + + /// + /// The "User not local; will forward" status code. + /// + UserNotLocalWillForward = 251, + + /// + /// The "cannot verify user; will attempt delivery" status code. + /// + CannotVerifyUserWillAttemptDelivery = 252, + + /// + /// The "authentication challenge" status code. + /// + AuthenticationChallenge = 334, + + /// + /// The "start mail input" status code. + /// + StartMailInput = 354, + + /// + /// The "service not available" status code. + /// + ServiceNotAvailable = 421, + + /// + /// The "password transition needed" status code. + /// + PasswordTransitionNeeded = 432, + + /// + /// The "mailbox busy" status code. + /// + MailboxBusy = 450, + + /// + /// The "error in processing" status code. + /// + ErrorInProcessing = 451, + + /// + /// The "insufficient storage" status code. + /// + InsufficientStorage = 452, + + /// + /// The "temporary authentication failure" status code. + /// + TemporaryAuthenticationFailure = 454, + + /// + /// The "command unrecognized" status code. + /// + CommandUnrecognized = 500, + + /// + /// The "syntax error" status code. + /// + SyntaxError = 501, + + /// + /// The "command not implemented" status code. + /// + CommandNotImplemented = 502, + + /// + /// The "bad command sequence" status code. + /// + BadCommandSequence = 503, + + /// + /// The "command parameter not implemented" status code. + /// + CommandParameterNotImplemented = 504, + + /// + /// The "authentication required" status code. + /// + AuthenticationRequired = 530, + + /// + /// The "authentication mechanism too weak" status code. + /// + AuthenticationMechanismTooWeak = 534, + + /// + /// The "authentication invalid credentials" status code. + /// + AuthenticationInvalidCredentials = 535, + + /// + /// The "encryption required for authentication mechanism" status code. + /// + EncryptionRequiredForAuthenticationMechanism = 538, + + /// + /// The "mailbox unavailable" status code. + /// + MailboxUnavailable = 550, + + /// + /// The "user not local try alternate path" status code. + /// + UserNotLocalTryAlternatePath = 551, + + /// + /// The "exceeded storage allocation" status code. + /// + ExceededStorageAllocation = 552, + + /// + /// The "mailbox name not allowed" status code. + /// + MailboxNameNotAllowed = 553, + + /// + /// The "transaction failed" status code. + /// + TransactionFailed = 554, + + /// + /// The "mail from/rcpt to parameters not recognized or not implemented" status code. + /// + MailFromOrRcptToParametersNotRecognizedOrNotImplemented = 555, + } +} diff --git a/src/MailKit/Net/Smtp/SmtpStream.cs b/src/MailKit/Net/Smtp/SmtpStream.cs new file mode 100644 index 0000000..e175373 --- /dev/null +++ b/src/MailKit/Net/Smtp/SmtpStream.cs @@ -0,0 +1,903 @@ +// +// SmtpStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Net.Sockets; +using System.Net.Security; +using System.Threading.Tasks; + +using MimeKit.IO; + +using Buffer = System.Buffer; +using NetworkStream = MailKit.Net.NetworkStream; + +namespace MailKit.Net.Smtp { + /// + /// A stream for communicating with an SMTP server. + /// + /// + /// A stream capable of reading SMTP server responses. + /// + class SmtpStream : Stream, ICancellableStream + { + static readonly Encoding Latin1; + static readonly Encoding UTF8; + const int BlockSize = 4096; + + // I/O buffering + readonly byte[] input = new byte[BlockSize]; + readonly byte[] output = new byte[BlockSize]; + int outputIndex; + + readonly IProtocolLogger logger; + int inputIndex, inputEnd; + bool disposed; + + static SmtpStream () + { + UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); + + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.GetEncoding (1252); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The underlying network stream. + /// The protocol logger. + public SmtpStream (Stream source, IProtocolLogger protocolLogger) + { + logger = protocolLogger; + IsConnected = true; + Stream = source; + } + + /// + /// Get or sets the underlying network stream. + /// + /// + /// Gets or sets the underlying network stream. + /// + /// The underlying network stream. + public Stream Stream { + get; internal set; + } + + /// + /// Get whether or not the stream is connected. + /// + /// + /// Gets whether or not the stream is connected. + /// + /// true if the stream is connected; otherwise, false. + public bool IsConnected { + get; private set; + } + + /// + /// Get whether the stream supports reading. + /// + /// + /// Gets whether the stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return Stream.CanRead; } + } + + /// + /// Get whether the stream supports writing. + /// + /// + /// Gets whether the stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return Stream.CanWrite; } + } + + /// + /// Get whether the stream supports seeking. + /// + /// + /// Gets whether the stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Get whether the stream supports I/O timeouts. + /// + /// + /// Gets whether the stream supports I/O timeouts. + /// + /// true if the stream supports I/O timeouts; otherwise, false. + public override bool CanTimeout { + get { return Stream.CanTimeout; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to read before timing out. + /// The read timeout. + public override int ReadTimeout { + get { return Stream.ReadTimeout; } + set { Stream.ReadTimeout = value; } + } + + /// + /// Get or set a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// + /// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// A value, in milliseconds, that determines how long the stream will attempt to write before timing out. + /// The write timeout. + public override int WriteTimeout { + get { return Stream.WriteTimeout; } + set { Stream.WriteTimeout = value; } + } + + /// + /// Get or set the position within the current stream. + /// + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return Stream.Position; } + set { throw new NotSupportedException (); } + } + + /// + /// Get the length of the stream, in bytes. + /// + /// + /// Gets the length of the stream, in bytes. + /// + /// A long value representing the length of the stream in bytes. + /// The length of the stream. + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Length { + get { return Stream.Length; } + } + + async Task ReadAheadAsync (bool doAsync, CancellationToken cancellationToken) + { + int left = inputEnd - inputIndex; + int index, nread; + + if (left > 0) { + if (inputIndex > 0) { + // move all of the remaining input to the beginning of the buffer + Buffer.BlockCopy (input, inputIndex, input, 0, left); + inputEnd = left; + inputIndex = 0; + } + } else { + inputIndex = 0; + inputEnd = 0; + } + + left = input.Length - inputEnd; + index = inputEnd; + + try { + var network = Stream as NetworkStream; + + cancellationToken.ThrowIfCancellationRequested (); + + if (doAsync) { + nread = await Stream.ReadAsync (input, index, left, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectRead, cancellationToken); + nread = Stream.Read (input, index, left); + } + + if (nread > 0) { + logger.LogServer (input, index, nread); + inputEnd += nread; + } else { + throw new SmtpProtocolException ("The SMTP server has unexpectedly disconnected."); + } + } catch { + IsConnected = false; + throw; + } + + return inputEnd - inputIndex; + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (SmtpStream)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { +#if false // Note: this code will never get called as we always use ReadResponse() instead. + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + int length = inputEnd - inputIndex; + int n; + + if (length < count && length <= ReadAheadSize) + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); + + length = inputEnd - inputIndex; + n = Math.Min (count, length); + + Buffer.BlockCopy (input, inputIndex, buffer, offset, n); + inputIndex += n; + + return n; +#else + throw new NotImplementedException (); +#endif + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + return Read (buffer, offset, count, CancellationToken.None); + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer. + /// The buffer offset. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { +#if false // Note: this code will never get called as we always use ReadResponse() instead. + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + int length = inputEnd - inputIndex; + int n; + + if (length < count && length <= ReadAheadSize) + await ReadAheadAsync (cancellationToken).ConfigureAwait (false); + + length = inputEnd - inputIndex; + n = Math.Min (count, length); + + Buffer.BlockCopy (input, inputIndex, buffer, offset, n); + inputIndex += n; + + return n; +#else + throw new NotImplementedException (); +#endif + } + + static bool TryParseInt32 (byte[] text, ref int index, int endIndex, out int value) + { + int startIndex = index; + + value = 0; + + while (index < endIndex && text[index] >= (byte) '0' && text[index] <= (byte) '9') + value = (value * 10) + (text[index++] - (byte) '0'); + + return index > startIndex; + } + + async Task ReadResponseAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + using (var memory = new MemoryStream ()) { + bool needInput = inputIndex == inputEnd; + bool complete = false; + bool newLine = true; + bool more = true; + int code = 0; + + do { + if (needInput) { + await ReadAheadAsync (doAsync, cancellationToken).ConfigureAwait (false); + needInput = false; + } + + complete = false; + + do { + int startIndex = inputIndex; + + if (newLine && inputIndex < inputEnd) { + int value; + + if (!TryParseInt32 (input, ref inputIndex, inputEnd, out value)) + throw new SmtpProtocolException ("Unable to parse status code returned by the server."); + + if (inputIndex == inputEnd) { + inputIndex = startIndex; + needInput = true; + break; + } + + if (code == 0) { + code = value; + } else if (value != code) { + throw new SmtpProtocolException ("The status codes returned by the server did not match."); + } + + newLine = false; + + if (input[inputIndex] != (byte) '\r' && input[inputIndex] != (byte) '\n') + more = input[inputIndex++] == (byte) '-'; + else + more = false; + + startIndex = inputIndex; + } + + while (inputIndex < inputEnd && input[inputIndex] != (byte) '\r' && input[inputIndex] != (byte) '\n') + inputIndex++; + + memory.Write (input, startIndex, inputIndex - startIndex); + + if (inputIndex < inputEnd && input[inputIndex] == (byte) '\r') + inputIndex++; + + if (inputIndex < inputEnd && input[inputIndex] == (byte) '\n') { + if (more) + memory.WriteByte (input[inputIndex]); + complete = true; + newLine = true; + inputIndex++; + } + } while (more && inputIndex < inputEnd); + + if (inputIndex == inputEnd) + needInput = true; + } while (more || !complete); + + string message = null; + + try { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + message = UTF8.GetString (memory.GetBuffer (), 0, (int) memory.Length); +#else + message = UTF8.GetString (memory.ToArray (), 0, (int) memory.Length); +#endif + } catch (DecoderFallbackException) { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + message = Latin1.GetString (memory.GetBuffer (), 0, (int) memory.Length); +#else + message = Latin1.GetString (memory.ToArray (), 0, (int) memory.Length); +#endif + } + + return new SmtpResponse ((SmtpStatusCode) code, message); + } + } + + /// + /// Read an SMTP server response. + /// + /// + /// Reads a full command response from the SMTP server. + /// + /// The response. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol error occurred. + /// + public SmtpResponse ReadResponse (CancellationToken cancellationToken) + { + return ReadResponseAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously read an SMTP server response. + /// + /// + /// Reads a full command response from the SMTP server. + /// + /// The response. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An SMTP protocol error occurred. + /// + public Task ReadResponseAsync (CancellationToken cancellationToken) + { + return ReadResponseAsync (true, cancellationToken); + } + + async Task WriteAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + try { + var network = NetworkStream.Get (Stream); + int index = offset; + int left = count; + + while (left > 0) { + int n = Math.Min (BlockSize - outputIndex, left); + + if (outputIndex > 0 || n < BlockSize) { + // append the data to the output buffer + Buffer.BlockCopy (buffer, index, output, outputIndex, n); + outputIndex += n; + index += n; + left -= n; + } + + if (outputIndex == BlockSize) { + // flush the output buffer + if (doAsync) { + await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, BlockSize); + } + logger.LogClient (output, 0, BlockSize); + outputIndex = 0; + } + + if (outputIndex == 0) { + // write blocks of data to the stream without buffering + while (left >= BlockSize) { + if (doAsync) { + await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false); + } else { + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (buffer, index, BlockSize); + } + logger.LogClient (buffer, index, BlockSize); + index += BlockSize; + left -= BlockSize; + } + } + } + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + WriteAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + Write (buffer, offset, count, CancellationToken.None); + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync (buffer, offset, count, true, cancellationToken); + } + + async Task FlushAsync (bool doAsync, CancellationToken cancellationToken) + { + CheckDisposed (); + + if (outputIndex == 0) + return; + + try { + if (doAsync) { + await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false); + await Stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } else { + var network = NetworkStream.Get (Stream); + + network?.Poll (SelectMode.SelectWrite, cancellationToken); + Stream.Write (output, 0, outputIndex); + Stream.Flush (); + } + logger.LogClient (output, 0, outputIndex); + outputIndex = 0; + } catch (Exception ex) { + IsConnected = false; + if (!(ex is OperationCanceledException)) + cancellationToken.ThrowIfCancellationRequested (); + throw; + } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Flush (CancellationToken cancellationToken) + { + FlushAsync (false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + Flush (CancellationToken.None); + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + return FlushAsync (true, cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + /// + /// Sets the length of the stream. + /// + /// The desired length of the stream in bytes. + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + IsConnected = false; + Stream.Dispose (); + } + + disposed = true; + + base.Dispose (disposing); + } + } +} diff --git a/src/MailKit/Net/SocketUtils.cs b/src/MailKit/Net/SocketUtils.cs new file mode 100644 index 0000000..62e2ea9 --- /dev/null +++ b/src/MailKit/Net/SocketUtils.cs @@ -0,0 +1,118 @@ +// +// SocketUtils.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace MailKit.Net +{ + static class SocketUtils + { + public static async Task ConnectAsync (string host, int port, IPEndPoint localEndPoint, bool doAsync, CancellationToken cancellationToken) + { + IPAddress[] ipAddresses; + + cancellationToken.ThrowIfCancellationRequested (); + + if (doAsync) { + ipAddresses = await Dns.GetHostAddressesAsync (host).ConfigureAwait (false); + } else { + ipAddresses = Dns.GetHostAddressesAsync (host).GetAwaiter ().GetResult (); + } + + for (int i = 0; i < ipAddresses.Length; i++) { + cancellationToken.ThrowIfCancellationRequested (); + + var socket = new Socket (ipAddresses[i].AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try { + if (localEndPoint != null) + socket.Bind (localEndPoint); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (doAsync || cancellationToken.CanBeCanceled) { + var tcs = new TaskCompletionSource (); + + using (var registration = cancellationToken.Register (() => tcs.TrySetCanceled (), false)) { + var ar = socket.BeginConnect (ipAddresses[i], port, e => tcs.TrySetResult (true), null); + + if (doAsync) + await tcs.Task.ConfigureAwait (false); + else + tcs.Task.GetAwaiter ().GetResult (); + + socket.EndConnect (ar); + } + } else { + socket.Connect (ipAddresses[i], port); + } +#else + socket.Connect (ipAddresses[i], port); +#endif + + return socket; + } catch (OperationCanceledException) { + socket.Dispose (); + throw; + } catch { + socket.Dispose (); + + if (i + 1 == ipAddresses.Length) + throw; + } + } + + throw new IOException (string.Format ("Failed to resolve host: {0}", host)); + } + + public static async Task ConnectAsync (string host, int port, IPEndPoint localEndPoint, int timeout, bool doAsync, CancellationToken cancellationToken) + { + using (var ts = new CancellationTokenSource (timeout)) { + using (var linked = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, ts.Token)) { + try { + return await ConnectAsync (host, port, localEndPoint, doAsync, linked.Token).ConfigureAwait (false); + } catch (OperationCanceledException) { + if (!cancellationToken.IsCancellationRequested) + throw new TimeoutException (); + throw; + } + } + } + } + + public static void Poll (Socket socket, SelectMode mode, CancellationToken cancellationToken) + { + do { + cancellationToken.ThrowIfCancellationRequested (); + // wait 1/4 second and then re-check for cancellation + } while (!socket.Poll (250000, mode)); + } + } +} diff --git a/src/MailKit/Net/SslStream.cs b/src/MailKit/Net/SslStream.cs new file mode 100644 index 0000000..cd2fb03 --- /dev/null +++ b/src/MailKit/Net/SslStream.cs @@ -0,0 +1,44 @@ +// +// SslStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Threading; +using System.Net.Security; +using System.Threading.Tasks; + +namespace MailKit.Net +{ + class SslStream : System.Net.Security.SslStream + { + public SslStream (Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback) : base (innerStream, leaveInnerStreamOpen, userCertificateValidationCallback) + { + } + + new public Stream InnerStream { + get { return base.InnerStream; } + } + } +} diff --git a/src/MailKit/NullProtocolLogger.cs b/src/MailKit/NullProtocolLogger.cs new file mode 100644 index 0000000..fe0fa47 --- /dev/null +++ b/src/MailKit/NullProtocolLogger.cs @@ -0,0 +1,113 @@ +// +// NullProtocolLogger.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// A protocol logger that does not log to anywhere. + /// + /// + /// By default, the , + /// , and + /// all use a + /// . + /// + public sealed class NullProtocolLogger : IProtocolLogger + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public NullProtocolLogger () + { + } + + #region IProtocolLogger implementation + + /// + /// Logs a connection to the specified URI. + /// + /// + /// This method does nothing. + /// + /// The URI. + public void LogConnect (Uri uri) + { + } + + /// + /// Logs a sequence of bytes sent by the client. + /// + /// + /// This method does nothing. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + public void LogClient (byte[] buffer, int offset, int count) + { + } + + /// + /// Logs a sequence of bytes sent by the server. + /// + /// + /// This method does nothing. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + public void LogServer (byte[] buffer, int offset, int count) + { + } + + #endregion + + #region IDisposable implementation + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + } + + #endregion + } +} diff --git a/src/MailKit/ProgressStream.cs b/src/MailKit/ProgressStream.cs new file mode 100644 index 0000000..9748a56 --- /dev/null +++ b/src/MailKit/ProgressStream.cs @@ -0,0 +1,185 @@ +// +// ProgressStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using MimeKit.IO; + +namespace MailKit { + class ProgressStream : Stream, ICancellableStream + { + readonly ICancellableStream cancellable; + + public ProgressStream (Stream source, Action update) + { + if (source == null) + throw new ArgumentNullException (nameof (source)); + + if (update == null) + throw new ArgumentNullException (nameof (update)); + + cancellable = source as ICancellableStream; + Source = source; + Update = update; + } + + public Stream Source { + get; private set; + } + + Action Update { + get; set; + } + + public override bool CanRead { + get { return Source.CanRead; } + } + + public override bool CanWrite { + get { return Source.CanWrite; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanTimeout { + get { return Source.CanTimeout; } + } + + public override long Length { + get { return Source.Length; } + } + + public override long Position { + get { return Source.Position; } + set { Seek (value, SeekOrigin.Begin); } + } + + public override int ReadTimeout { + get { return Source.ReadTimeout; } + set { Source.ReadTimeout = value; } + } + + public override int WriteTimeout { + get { return Source.WriteTimeout; } + set { Source.WriteTimeout = value; } + } + + public int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int n; + + if (cancellable != null) { + if ((n = cancellable.Read (buffer, offset, count, cancellationToken)) > 0) + Update (n); + } else { + if ((n = Source.Read (buffer, offset, count)) > 0) + Update (n); + } + + return n; + } + + public override int Read (byte[] buffer, int offset, int count) + { + int n; + + if ((n = Source.Read (buffer, offset, count)) > 0) + Update (n); + + return n; + } + + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int n; + + if ((n = await Source.ReadAsync (buffer, offset, count, cancellationToken).ConfigureAwait (false)) > 0) + Update (n); + + return n; + } + + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellable != null) + cancellable.Write (buffer, offset, count, cancellationToken); + else + Source.Write (buffer, offset, count); + + if (count > 0) + Update (count); + } + + public override void Write (byte[] buffer, int offset, int count) + { + Source.Write (buffer, offset, count); + + if (count > 0) + Update (count); + } + + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Source.WriteAsync (buffer, offset, count, cancellationToken).ConfigureAwait (false); + + if (count > 0) + Update (count); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException ("The stream does not support seeking."); + } + + public void Flush (CancellationToken cancellationToken) + { + if (cancellable != null) + cancellable.Flush (cancellationToken); + else + Source.Flush (); + } + + public override void Flush () + { + Source.Flush (); + } + + public override Task FlushAsync (CancellationToken cancellationToken) + { + return Source.FlushAsync (cancellationToken); + } + + public override void SetLength (long value) + { + throw new NotSupportedException ("The stream does not support resizing."); + } + } +} diff --git a/src/MailKit/Properties/AssemblyInfo.cs b/src/MailKit/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b4f4c12 --- /dev/null +++ b/src/MailKit/Properties/AssemblyInfo.cs @@ -0,0 +1,84 @@ +// +// AssemblyInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle ("MailKit")] +[assembly: AssemblyDescription ("A cross-platform mail client library.")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany (".NET Foundation")] +[assembly: AssemblyProduct ("MailKit")] +[assembly: AssemblyCopyright ("Copyright © 2013-2020 .NET Foundation and Contributors")] +[assembly: AssemblyTrademark (".NET Foundation")] +[assembly: AssemblyCulture ("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible (true)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid ("2fe79b66-d107-45da-9493-175f59c4a53c")] +[assembly: InternalsVisibleTo ("UnitTests, PublicKey=002400000480000094000000060200" + + "0000240000525341310004000011000000cde209732ce60a8fa70ee643cb32e9bf8149b61018c5" + + "b166489b8a5cae44f1f88ca97ab9d9e035421933a6f0d556acc7c2219ae1464e35386ca1e239aa" + + "42508b9edbb4164bfa82aa2a0f4cd983d9e5ba2acfe08a10a2093e2b2bf8408eef43114db89b39" + + "99c59af1d3dc2c9f0cdbf51074e9a482cf09c9116ae1c5543ce8ff9b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Micro Version +// Build Number +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +// +// Note: AssemblyVersion is what the CLR matches against at runtime, so be careful +// about updating it. The AssemblyFileVersion is the official release version while +// the AssemblyInformationalVersion is just used as a display version. +// +// Based on my current understanding, AssemblyVersion is essentially the "API Version" +// and so should only be updated when the API changes. The other 2 Version attributes +// represent the "Release Version". +// +// Making releases: +// +// If any classes, methods, or enum values have been added, bump the Micro Version +// in all version attributes and set the Build Number back to 0. +// +// If there have only been bug fixes, bump the Micro Version and/or the Build Number +// in the AssemblyFileVersion attribute. +[assembly: AssemblyInformationalVersion ("2.7.0.0")] +[assembly: AssemblyFileVersion ("2.7.0.0")] +[assembly: AssemblyVersion ("2.7.0.0")] diff --git a/src/MailKit/ProtocolException.cs b/src/MailKit/ProtocolException.cs new file mode 100644 index 0000000..88b08c5 --- /dev/null +++ b/src/MailKit/ProtocolException.cs @@ -0,0 +1,100 @@ +// +// ProtocolException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when there is a protocol error. + /// + /// + /// A can be thrown by any of the various client + /// methods in MailKit. + /// Since many protocol exceptions are fatal, it is important to check whether + /// or not the client is still connected using the + /// property when this exception is thrown. + /// +#if SERIALIZABLE + [Serializable] +#endif + public abstract class ProtocolException : Exception + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The streaming context. + [SecuritySafeCritical] + protected ProtocolException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + protected ProtocolException (string message, Exception innerException) : base (message, innerException) + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + protected ProtocolException (string message) : base (message) + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + protected ProtocolException () + { + HelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#ProtocolLog"; + } + } +} diff --git a/src/MailKit/ProtocolLogger.cs b/src/MailKit/ProtocolLogger.cs new file mode 100644 index 0000000..f0682cf --- /dev/null +++ b/src/MailKit/ProtocolLogger.cs @@ -0,0 +1,341 @@ +// +// ProtocolLogger.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; + +namespace MailKit { + /// + /// A default protocol logger for logging the communication between a client and server. + /// + /// + /// A default protocol logger for logging the communication between a client and server. + /// + /// + /// + /// + public class ProtocolLogger : IProtocolLogger + { + static byte[] defaultClientPrefix = Encoding.ASCII.GetBytes ("C: "); + static byte[] defaultServerPrefix = Encoding.ASCII.GetBytes ("S: "); + + byte[] clientPrefix = defaultClientPrefix; + byte[] serverPrefix = defaultServerPrefix; + readonly Stream stream; + readonly bool leaveOpen; + bool clientMidline; + bool serverMidline; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new to log to a specified file. The file is created if it does not exist. + /// + /// + /// + /// + /// The file name. + /// true if the file should be appended to; otherwise, false. Defaults to true. + public ProtocolLogger (string fileName, bool append = true) + { + stream = File.Open (fileName, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new to log to a specified stream. + /// + /// The stream. + /// true if the stream should be left open after the protocol logger is disposed. + public ProtocolLogger (Stream stream, bool leaveOpen = false) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + this.leaveOpen = leaveOpen; + this.stream = stream; + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~ProtocolLogger () + { + Dispose (false); + } + + /// + /// Get the log stream. + /// + /// + /// Gets the log stream. + /// + /// The log sstream. + public Stream Stream { + get { return stream; } + } + + /// + /// Get or set the default client prefix to use when creating new instances. + /// + /// + /// Get or set the default client prefix to use when creating new instances. + /// + /// The default client prefix. + public static string DefaultClientPrefix + { + get { return Encoding.UTF8.GetString (defaultClientPrefix); } + set { defaultClientPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the default server prefix to use when creating new instances. + /// + /// + /// Get or set the default server prefix to use when creating new instances. + /// + /// The default server prefix. + public static string DefaultServerPrefix + { + get { return Encoding.UTF8.GetString (defaultServerPrefix); } + set { defaultServerPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the client prefix to use when logging client messages. + /// + /// + /// Gets or sets the client prefix to use when logging client messages. + /// + /// The client prefix. + public string ClientPrefix + { + get { return Encoding.UTF8.GetString (clientPrefix); } + set { clientPrefix = Encoding.UTF8.GetBytes (value); } + } + + /// + /// Get or set the server prefix to use when logging server messages. + /// + /// + /// Gets or sets the server prefix to use when logging server messages. + /// + /// The server prefix. + public string ServerPrefix + { + get { return Encoding.UTF8.GetString (serverPrefix); } + set { serverPrefix = Encoding.UTF8.GetBytes (value); } + } + + #region IProtocolLogger implementation + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + void Log (byte[] prefix, ref bool midline, byte[] buffer, int offset, int count) + { + int endIndex = offset + count; + int index = offset; + int start; + + while (index < endIndex) { + start = index; + + while (index < endIndex && buffer[index] != (byte) '\n') + index++; + + if (!midline) + stream.Write (prefix, 0, prefix.Length); + + if (index < endIndex && buffer[index] == (byte) '\n') { + midline = false; + index++; + } else { + midline = true; + } + + stream.Write (buffer, start, index - start); + } + + stream.Flush (); + } + + /// + /// Logs a connection to the specified URI. + /// + /// + /// Logs a connection to the specified URI. + /// + /// The URI. + /// + /// is null. + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + public void LogConnect (Uri uri) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + var message = string.Format ("Connected to {0}\r\n", uri); + var buf = Encoding.ASCII.GetBytes (message); + + if (clientMidline || serverMidline) { + stream.WriteByte ((byte) '\r'); + stream.WriteByte ((byte) '\n'); + clientMidline = false; + serverMidline = false; + } + + stream.Write (buf, 0, buf.Length); + stream.Flush (); + } + + /// + /// Logs a sequence of bytes sent by the client. + /// + /// + /// Logs a sequence of bytes sent by the client. + /// is called by the upon every successful + /// write operation to its underlying network stream, passing the exact same , + /// , and arguments to the logging function. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + public void LogClient (byte[] buffer, int offset, int count) + { + ValidateArguments (buffer, offset, count); + + Log (clientPrefix, ref clientMidline, buffer, offset, count); + } + + /// + /// Logs a sequence of bytes sent by the server. + /// + /// + /// Logs a sequence of bytes sent by the server. + /// is called by the upon every successful + /// read of its underlying network stream with the exact buffer that was read. + /// + /// The buffer to log. + /// The offset of the first byte to log. + /// The number of bytes to log. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes strting + /// at the specified . + /// + /// + /// The logger has been disposed. + /// + /// + /// An I/O error occurred. + /// + public void LogServer (byte[] buffer, int offset, int count) + { + ValidateArguments (buffer, offset, count); + + Log (serverPrefix, ref serverMidline, buffer, offset, count); + } + + #endregion + + #region IDisposable implementation + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected virtual void Dispose (bool disposing) + { + if (disposing && !leaveOpen) + stream.Dispose (); + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After calling + /// , you must release all references to the so the garbage + /// collector can reclaim the memory that the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + #endregion + } +} diff --git a/src/MailKit/Search/AnnotationSearchQuery.cs b/src/MailKit/Search/AnnotationSearchQuery.cs new file mode 100644 index 0000000..07805b4 --- /dev/null +++ b/src/MailKit/Search/AnnotationSearchQuery.cs @@ -0,0 +1,105 @@ +// +// AnnotationSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search +{ + /// + /// An annotation-based search query. + /// + /// + /// An annotation-based search query. + /// + public class AnnotationSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new annotation-based search query. + /// + /// The annotation entry. + /// The annotation attribute. + /// The annotation attribute value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid attribute for searching. + /// + public AnnotationSearchQuery (AnnotationEntry entry, AnnotationAttribute attribute, string value) : base (SearchTerm.Annotation) + { + if (entry == null) + throw new ArgumentNullException (nameof (entry)); + + if (attribute == null) + throw new ArgumentNullException (nameof (attribute)); + + if (attribute.Name != "value") + throw new ArgumentException ("Only the \"value\", \"value.priv\", and \"value.shared\" attributes can be searched.", nameof (attribute)); + + Attribute = attribute; + Entry = entry; + Value = value; + } + + /// + /// Get the annotation entry. + /// + /// + /// Gets the annotation entry. + /// + /// The annotation entry. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation attribute. + /// + /// + /// Gets the annotation attribute. + /// + /// The annotation attribute. + public AnnotationAttribute Attribute { + get; private set; + } + + /// + /// Get the annotation attribute value. + /// + /// + /// Gets the annotation attribute value. + /// + /// The annotation attribute value. + public string Value { + get; private set; + } + } +} diff --git a/src/MailKit/Search/BinarySearchQuery.cs b/src/MailKit/Search/BinarySearchQuery.cs new file mode 100644 index 0000000..9bbd071 --- /dev/null +++ b/src/MailKit/Search/BinarySearchQuery.cs @@ -0,0 +1,100 @@ +// +// BinarySearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// A binary search query such as an AND or OR expression. + /// + /// + /// A binary search query such as an AND or OR expression. + /// + public class BinarySearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new binary search query. + /// + /// THe search term. + /// The left expression. + /// The right expression. + /// + /// is null. + /// -or- + /// is null. + /// + public BinarySearchQuery (SearchTerm term, SearchQuery left, SearchQuery right) : base (term) + { + if (left == null) + throw new ArgumentNullException (nameof (left)); + + if (right == null) + throw new ArgumentNullException (nameof (right)); + + Right = right; + Left = left; + } + + /// + /// Gets the left operand of the expression. + /// + /// + /// Gets the left operand of the expression. + /// + /// The left operand. + public SearchQuery Left { + get; private set; + } + + /// + /// Gets the right operand of the expression. + /// + /// + /// Gets the right operand of the expression. + /// + /// The right operand. + public SearchQuery Right { + get; private set; + } + + internal override SearchQuery Optimize (ISearchQueryOptimizer optimizer) + { + var right = Right.Optimize (optimizer); + var left = Left.Optimize (optimizer); + SearchQuery binary; + + if (left != Left || right != Right) + binary = new BinarySearchQuery (Term, left, right); + else + binary = this; + + return optimizer.Reduce (binary); + } + } +} diff --git a/src/MailKit/Search/DateSearchQuery.cs b/src/MailKit/Search/DateSearchQuery.cs new file mode 100644 index 0000000..c8227df --- /dev/null +++ b/src/MailKit/Search/DateSearchQuery.cs @@ -0,0 +1,62 @@ +// +// DateSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// A date-based search query. + /// + /// + /// A date-based search query. + /// + public class DateSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new date-based search query. + /// + /// The search term. + /// The date. + public DateSearchQuery (SearchTerm term, DateTime date) : base (term) + { + Date = date; + } + + /// + /// Gets the date value of the search query. + /// + /// + /// Gets the date value of the search query. + /// + /// The date. + public DateTime Date { + get; private set; + } + } +} diff --git a/src/MailKit/Search/FilterSearchQuery.cs b/src/MailKit/Search/FilterSearchQuery.cs new file mode 100644 index 0000000..80e3062 --- /dev/null +++ b/src/MailKit/Search/FilterSearchQuery.cs @@ -0,0 +1,94 @@ +// +// FilterSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search +{ + /// + /// A filter-based search query. + /// + /// + /// A filter-based search query. + /// + public class FilterSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A search query that references a predefined filter. + /// + /// The name of the filter. + /// + /// is null. + /// + /// + /// is empty. + /// + public FilterSearchQuery (string name) : base (SearchTerm.Filter) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("The filter name cannot be empty.", nameof (name)); + + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A search query that references a predefined filter. + /// + /// The metadata tag representing the filter. + /// + /// does not reference a valid filter. + /// + public FilterSearchQuery (MetadataTag filter) : base (SearchTerm.Filter) + { + if (filter.Id.StartsWith ("/private/filters/values/", StringComparison.Ordinal)) + Name = filter.Id.Substring ("/private/filters/values/".Length); + else if (filter.Id.StartsWith ("/shared/filters/values/", StringComparison.Ordinal)) + Name = filter.Id.Substring ("/shared/filters/values/".Length); + else + throw new ArgumentException ("Metadata tag does not reference a valid filter.", nameof (filter)); + } + + /// + /// Get the name of the filter. + /// + /// + /// Gets the name of the filter. + /// + /// The name of the filter. + public string Name { + get; private set; + } + } +} diff --git a/src/MailKit/Search/HeaderSearchQuery.cs b/src/MailKit/Search/HeaderSearchQuery.cs new file mode 100644 index 0000000..c653a81 --- /dev/null +++ b/src/MailKit/Search/HeaderSearchQuery.cs @@ -0,0 +1,91 @@ +// +// HeaderSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// A header-based search query. + /// + /// + /// A header-based search query. + /// + public class HeaderSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new header search query. + /// + /// The header field name. + /// The value to match against. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + public HeaderSearchQuery (string field, string value) : base (SearchTerm.HeaderContains) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + if (field.Length == 0) + throw new ArgumentException ("Cannot search an empty header field name.", nameof (field)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Field = field; + Value = value; + } + + /// + /// Gets the header field name. + /// + /// + /// Gets the header field name. + /// + /// The header field. + public string Field { + get; private set; + } + + /// + /// Gets the value to match against. + /// + /// + /// Gets the value to match against. + /// + /// The value. + public string Value { + get; private set; + } + } +} diff --git a/src/MailKit/Search/ISearchQueryOptimizer.cs b/src/MailKit/Search/ISearchQueryOptimizer.cs new file mode 100644 index 0000000..b55f346 --- /dev/null +++ b/src/MailKit/Search/ISearchQueryOptimizer.cs @@ -0,0 +1,32 @@ +// +// ISearchQueryOptimizer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Search { + interface ISearchQueryOptimizer + { + SearchQuery Reduce (SearchQuery expr); + } +} diff --git a/src/MailKit/Search/NumericSearchQuery.cs b/src/MailKit/Search/NumericSearchQuery.cs new file mode 100644 index 0000000..5bd0420 --- /dev/null +++ b/src/MailKit/Search/NumericSearchQuery.cs @@ -0,0 +1,60 @@ +// +// NumericSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Search { + /// + /// A numeric search query. + /// + /// + /// A numeric search query. + /// + public class NumericSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new numeric search query. + /// + /// The search term. + /// The numeric value. + public NumericSearchQuery (SearchTerm term, ulong value) : base (term) + { + Value = value; + } + + /// + /// Gets the numeric value to match against. + /// + /// + /// Gets the numeric value to match against. + /// + /// The numeric value. + public ulong Value { + get; private set; + } + } +} diff --git a/src/MailKit/Search/OrderBy.cs b/src/MailKit/Search/OrderBy.cs new file mode 100644 index 0000000..1d8637b --- /dev/null +++ b/src/MailKit/Search/OrderBy.cs @@ -0,0 +1,223 @@ +// +// OrderBy.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// Specifies a sort order for search results. + /// + /// + /// You can combine multiple rules to specify the sort + /// order that + /// should return the results in. + /// + public class OrderBy + { + /// + /// Initializes a new instance of the class. + /// + /// The field to sort by. + /// The sort order. + /// + /// cannot be . + /// + public OrderBy (OrderByType type, SortOrder order) + { + if (order == SortOrder.None) + throw new ArgumentOutOfRangeException (nameof (order)); + + Order = order; + Type = type; + } + + /// + /// Gets the field used for sorting. + /// + /// + /// Gets the field used for sorting. + /// + /// The field used for sorting. + public OrderByType Type { + get; private set; + } + + /// + /// Gets the sort order. + /// + /// + /// Gets the sort order. + /// + /// The sort order. + public SortOrder Order { + get; private set; + } + + /// + /// Sort results by arrival date in ascending order. + /// + /// + /// Sort results by arrival date in ascending order. + /// + public static readonly OrderBy Arrival = new OrderBy (OrderByType.Arrival, SortOrder.Ascending); + + /// + /// Sort results by arrival date in desending order. + /// + /// + /// Sort results by arrival date in desending order. + /// + public static readonly OrderBy ReverseArrival = new OrderBy (OrderByType.Arrival, SortOrder.Descending); + + /// + /// Sort results by the first email address in the Cc header in ascending order. + /// + /// + /// Sort results by the first email address in the Cc header in ascending order. + /// + public static readonly OrderBy Cc = new OrderBy (OrderByType.Cc, SortOrder.Ascending); + + /// + /// Sort results by the first email address in the Cc header in descending order. + /// + /// + /// Sort results by the first email address in the Cc header in descending order. + /// + public static readonly OrderBy ReverseCc = new OrderBy (OrderByType.Cc, SortOrder.Descending); + + /// + /// Sort results by the sent date in ascending order. + /// + /// + /// Sort results by the sent date in ascending order. + /// + public static readonly OrderBy Date = new OrderBy (OrderByType.Date, SortOrder.Ascending); + + /// + /// Sort results by the sent date in descending order. + /// + /// + /// Sort results by the sent date in descending order. + /// + public static readonly OrderBy ReverseDate = new OrderBy (OrderByType.Date, SortOrder.Descending); + + /// + /// Sort results by the first email address in the From header in ascending order. + /// + /// + /// Sort results by the first email address in the From header in ascending order. + /// + public static readonly OrderBy From = new OrderBy (OrderByType.From, SortOrder.Ascending); + + /// + /// Sort results by the first email address in the From header in descending order. + /// + /// + /// Sort results by the first email address in the From header in descending order. + /// + public static readonly OrderBy ReverseFrom = new OrderBy (OrderByType.From, SortOrder.Descending); + + /// + /// Sort results by the first display name in the From header in ascending order. + /// + /// + /// Sort results by the first display name in the From header in ascending order. + /// + public static readonly OrderBy DisplayFrom = new OrderBy (OrderByType.DisplayFrom, SortOrder.Ascending); + + /// + /// Sort results by the first display name in the From header in descending order. + /// + /// + /// Sort results by the first display name in the From header in descending order. + /// + public static readonly OrderBy ReverseDisplayFrom = new OrderBy (OrderByType.DisplayFrom, SortOrder.Descending); + + /// + /// Sort results by the message size in ascending order. + /// + /// + /// Sort results by the message size in ascending order. + /// + public static readonly OrderBy Size = new OrderBy (OrderByType.Size, SortOrder.Ascending); + + /// + /// Sort results by the message size in descending order. + /// + /// + /// Sort results by the message size in descending order. + /// + public static readonly OrderBy ReverseSize = new OrderBy (OrderByType.Size, SortOrder.Descending); + + /// + /// Sort results by the Subject header in ascending order. + /// + /// + /// Sort results by the Subject header in ascending order. + /// + public static readonly OrderBy Subject = new OrderBy (OrderByType.Subject, SortOrder.Ascending); + + /// + /// Sort results by the Subject header in descending order. + /// + /// + /// Sort results by the Subject header in descending order. + /// + public static readonly OrderBy ReverseSubject = new OrderBy (OrderByType.Subject, SortOrder.Descending); + + /// + /// Sort results by the first email address in the To header in ascending order. + /// + /// + /// Sort results by the first email address in the To header in ascending order. + /// + public static readonly OrderBy To = new OrderBy (OrderByType.To, SortOrder.Ascending); + + /// + /// Sort results by the first email address in the To header in descending order. + /// + /// + /// Sort results by the first email address in the To header in descending order. + /// + public static readonly OrderBy ReverseTo = new OrderBy (OrderByType.To, SortOrder.Descending); + + /// + /// Sort results by the first display name in the To header in ascending order. + /// + /// + /// Sort results by the first display name in the To header in ascending order. + /// + public static readonly OrderBy DisplayTo = new OrderBy (OrderByType.DisplayTo, SortOrder.Ascending); + + /// + /// Sort results by the first display name in the To header in descending order. + /// + /// + /// Sort results by the first display name in the To header in descending order. + /// + public static readonly OrderBy ReverseDisplayTo = new OrderBy (OrderByType.DisplayTo, SortOrder.Descending); + } +} diff --git a/src/MailKit/Search/OrderByAnnotation.cs b/src/MailKit/Search/OrderByAnnotation.cs new file mode 100644 index 0000000..e662b33 --- /dev/null +++ b/src/MailKit/Search/OrderByAnnotation.cs @@ -0,0 +1,91 @@ +// +// OrderByAnnotation.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// Specifies an annotation-based sort order for search results. + /// + /// + /// You can combine multiple rules to specify the sort + /// order that + /// should return the results in. + /// + public class OrderByAnnotation : OrderBy + { + /// + /// Initializes a new instance of the class. + /// + /// The annotation entry to sort by. + /// The annotation attribute to use for sorting. + /// The sort order. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid attribute for sorting. + /// + public OrderByAnnotation (AnnotationEntry entry, AnnotationAttribute attribute, SortOrder order) : base (OrderByType.Annotation, order) + { + if (entry == null) + throw new ArgumentNullException (nameof (entry)); + + if (attribute == null) + throw new ArgumentNullException (nameof (attribute)); + + if (attribute.Name != "value" || attribute.Scope == AnnotationScope.Both) + throw new ArgumentException ("Only the \"value.priv\" and \"value.shared\" attributes can be used for sorting.", nameof (attribute)); + + Entry = entry; + Attribute = attribute; + } + + /// + /// Get the annotation entry. + /// + /// + /// Gets the annotation entry. + /// + /// The annotation entry. + public AnnotationEntry Entry { + get; private set; + } + + /// + /// Get the annotation attribute. + /// + /// + /// Gets the annotation attribute. + /// + /// The annotation attribute. + public AnnotationAttribute Attribute { + get; private set; + } + } +} diff --git a/src/MailKit/Search/OrderByType.cs b/src/MailKit/Search/OrderByType.cs new file mode 100644 index 0000000..bbb0508 --- /dev/null +++ b/src/MailKit/Search/OrderByType.cs @@ -0,0 +1,90 @@ +// +// OrderByType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Search { + /// + /// The field to sort by. + /// + /// + /// The field to sort by. + /// + public enum OrderByType { + /// + /// Sort by an annotation value. + /// + Annotation, + + /// + /// Sort by the arrival date. + /// + Arrival, + + /// + /// Sort by the Cc header. + /// + Cc, + + /// + /// Sort by the Date header. + /// + Date, + + /// + /// Sort by the Display Name of the From header. + /// + DisplayFrom, + + /// + /// Sort by the Display Name of the To header. + /// + DisplayTo, + + /// + /// Sort by the From header. + /// + From, + + /// + /// Sort by the mod-sequence. + /// + ModSeq, + + /// + /// Sort by the message size. + /// + Size, + + /// + /// Sort by the message subject. + /// + Subject, + + /// + /// Sort by the To header. + /// + To + } +} diff --git a/src/MailKit/Search/SearchOptions.cs b/src/MailKit/Search/SearchOptions.cs new file mode 100644 index 0000000..f1de5a2 --- /dev/null +++ b/src/MailKit/Search/SearchOptions.cs @@ -0,0 +1,69 @@ +// +// SearchOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search { + /// + /// Advanced search options. + /// + /// + /// Advanced search options. + /// + [Flags] + public enum SearchOptions { + /// + /// No options specified. + /// + None = 0, + + /// + /// Returns all of the matching unique identifiers. + /// + All = 1 << 0, + + /// + /// Returns the number of messages that match the search query. + /// + Count = 1 << 1, + + /// + /// Returns the minimum unique identifier of the messages that match the search query. + /// + Min = 1 << 2, + + /// + /// Returns the maximum unique identifier of the messages that match the search query. + /// + Max = 1 << 3, + + /// + /// Returns the relevancy scores of the messages that match the query. Can only be used + /// when using FUZZY search. + /// + Relevancy = 1 << 4 + } +} diff --git a/src/MailKit/Search/SearchQuery.cs b/src/MailKit/Search/SearchQuery.cs new file mode 100644 index 0000000..8ecf20c --- /dev/null +++ b/src/MailKit/Search/SearchQuery.cs @@ -0,0 +1,1136 @@ +// +// SearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit.Search { + /// + /// A specialized query for searching messages in a . + /// + /// + /// A specialized query for searching messages in a . + /// + public class SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new that matches all messages. + /// + public SearchQuery () : this (SearchTerm.All) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new with the specified search term. + /// + /// The search term. + protected SearchQuery (SearchTerm term) + { + Term = term; + } + + /// + /// Get the search term used by the search query. + /// + /// + /// Gets the search term used by the search query. + /// + /// The term. + public SearchTerm Term { + get; private set; + } + + /// + /// Match all messages in the folder. + /// + /// + /// Matches all messages in the folder. + /// + public static readonly SearchQuery All = new SearchQuery (SearchTerm.All); + + /// + /// Create a conditional AND operation. + /// + /// + /// A conditional AND operation only evaluates the second operand if the first operand evaluates to true. + /// + /// A representing the conditional AND operation. + /// The first operand. + /// The second operand. + /// + /// is null. + /// -or- + /// is null. + /// + public static BinarySearchQuery And (SearchQuery left, SearchQuery right) + { + if (left == null) + throw new ArgumentNullException (nameof (left)); + + if (right == null) + throw new ArgumentNullException (nameof (right)); + + return new BinarySearchQuery (SearchTerm.And, left, right); + } + + /// + /// Create a conditional AND operation. + /// + /// + /// A conditional AND operation only evaluates the second operand if the first operand evaluates to true. + /// + /// A representing the conditional AND operation. + /// An additional query to execute. + /// + /// is null. + /// + public BinarySearchQuery And (SearchQuery expr) + { + if (expr == null) + throw new ArgumentNullException (nameof (expr)); + + return new BinarySearchQuery (SearchTerm.And, this, expr); + } + + /// + /// Match messages with the specified annotation. + /// + /// + /// Matches messages with the specified annotation. + /// This feature is not supported by all IMAP servers. + /// + /// The annotation entry. + /// The annotation attribute. + /// The annotation attribute value. + /// A . + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid attribute for searching. + /// + public static AnnotationSearchQuery AnnotationsContain (AnnotationEntry entry, AnnotationAttribute attribute, string value) + { + return new AnnotationSearchQuery (entry, attribute, value); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Answered = new SearchQuery (SearchTerm.Answered); + + /// + /// Match messages where the Bcc header contains the specified text. + /// + /// + /// Matches messages where the Bcc header contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery BccContains (string text) + { + return new TextSearchQuery (SearchTerm.BccContains, text); + } + + /// + /// Match messages where the message body contains the specified text. + /// + /// + /// Matches messages where the message body contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery BodyContains (string text) + { + return new TextSearchQuery (SearchTerm.BodyContains, text); + } + + /// + /// Match messages where the Cc header contains the specified text. + /// + /// + /// Matches messages where the Cc header contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery CcContains (string text) + { + return new TextSearchQuery (SearchTerm.CcContains, text); + } + + /// + /// Match messages that have mod-sequence values greater than or equal to the specified mod-sequence value. + /// + /// + /// Matches messages that have mod-sequence values greater than or equal to the specified mod-sequence value. + /// + /// A . + /// The mod-sequence value. + public static SearchQuery ChangedSince (ulong modseq) + { + return new NumericSearchQuery (SearchTerm.ModSeq, modseq); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Deleted = new SearchQuery (SearchTerm.Deleted); + + /// + /// Match messages that were delivered after the specified date. + /// + /// + /// Matches messages that were delivered after the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery DeliveredAfter (DateTime date) + { + return new DateSearchQuery (SearchTerm.DeliveredAfter, date); + } + + /// + /// Match messages that were delivered before the specified date. + /// + /// + /// Matches messages that were delivered before the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery DeliveredBefore (DateTime date) + { + return new DateSearchQuery (SearchTerm.DeliveredBefore, date); + } + + /// + /// Match messages that were delivered on the specified date. + /// + /// + /// Matches messages that were delivered on the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery DeliveredOn (DateTime date) + { + return new DateSearchQuery (SearchTerm.DeliveredOn, date); + } + + /// + /// Match messages that do not have the specified custom flag set. + /// + /// + /// Matches messages that do not have the specified custom flag set. + /// + /// A . + /// The custom flag. + /// + /// is null. + /// + /// + /// is empty. + /// + [Obsolete ("Use NotKeyword() instead.")] + public static TextSearchQuery DoesNotHaveCustomFlag (string flag) + { + return NotKeyword (flag); + } + + /// + /// Match messages that do not have any of the specified custom flags set. + /// + /// + /// Matches messages that do not have any of the specified custom flags set. + /// + /// A . + /// The custom flags. + /// + /// is null. + /// + /// + /// One or more of the is null or empty. + /// -or- + /// No custom flags were given. + /// + [Obsolete ("Use NotKeywords() instead.")] + public static SearchQuery DoesNotHaveCustomFlags (IEnumerable flags) + { + return NotKeywords (flags); + } + + /// + /// Match messages that do not have any of the specified flags set. + /// + /// + /// Matches messages that do not have any of the specified flags set. + /// + /// A . + /// The message flags. + /// + /// does not specify any valid message flags. + /// + [Obsolete ("Use NotFlags() instead.")] + public static SearchQuery DoesNotHaveFlags (MessageFlags flags) + { + return NotFlags (flags); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Draft = new SearchQuery (SearchTerm.Draft); + + /// + /// Match messages using a saved search filter. + /// + /// + /// Matches messages using a saved search filter. + /// + /// A . + /// The name of the saved search. + public static SearchQuery Filter (string name) + { + return new FilterSearchQuery (name); + } + + /// + /// Match messages using a saved search filter. + /// + /// + /// Matches messages using a saved search filter. + /// + /// A . + /// The name of the saved search. + public static SearchQuery Filter (MetadataTag filter) + { + return new FilterSearchQuery (filter); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Flagged = new SearchQuery (SearchTerm.Flagged); + + /// + /// Match messages where the From header contains the specified text. + /// + /// + /// Matches messages where the From header contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery FromContains (string text) + { + return new TextSearchQuery (SearchTerm.FromContains, text); + } + + /// + /// Apply a fuzzy matching algorithm to the specified expression. + /// + /// + /// Applies a fuzzy matching algorithm to the specified expression. + /// This feature is not supported by all IMAP servers. + /// + /// A . + /// The expression + /// + /// is null. + /// + public static UnarySearchQuery Fuzzy (SearchQuery expr) + { + if (expr == null) + throw new ArgumentNullException (nameof (expr)); + + return new UnarySearchQuery (SearchTerm.Fuzzy, expr); + } + + /// + /// Match messages that have the specified custom flag set. + /// + /// + /// Matches messages that have the specified custom flag set. + /// + /// A . + /// The custom flag. + /// + /// is null. + /// + /// + /// is empty. + /// + [Obsolete ("Use HasKeyword() instead.")] + public static TextSearchQuery HasCustomFlag (string flag) + { + return HasKeyword (flag); + } + + /// + /// Match messages that have the specified custom flags set. + /// + /// + /// Matches messages that have the specified custom flags set. + /// + /// A . + /// The custom flags. + /// + /// is null. + /// + /// + /// One or more of the is null or empty. + /// -or- + /// No custom flags were given. + /// + [Obsolete ("Use HasKeywords() instead.")] + public static SearchQuery HasCustomFlags (IEnumerable flags) + { + return HasKeywords (flags); + } + + /// + /// Match messages that have the specified flags set. + /// + /// + /// Matches messages that have the specified flags set. + /// + /// A . + /// The message flags. + /// + /// does not specify any valid message flags. + /// + public static SearchQuery HasFlags (MessageFlags flags) + { + var list = new List (); + + if ((flags & MessageFlags.Seen) != 0) + list.Add (Seen); + if ((flags & MessageFlags.Answered) != 0) + list.Add (Answered); + if ((flags & MessageFlags.Flagged) != 0) + list.Add (Flagged); + if ((flags & MessageFlags.Deleted) != 0) + list.Add (Deleted); + if ((flags & MessageFlags.Draft) != 0) + list.Add (Draft); + if ((flags & MessageFlags.Recent) != 0) + list.Add (Recent); + + if (list.Count == 0) + throw new ArgumentException ("No flags specified.", nameof (flags)); + + var query = list[0]; + for (int i = 1; i < list.Count; i++) + query = query.And (list[i]); + + return query; + } + + /// + /// Match messages that do not have any of the specified flags set. + /// + /// + /// Matches messages that do not have any of the specified flags set. + /// + /// A . + /// The message flags. + /// + /// does not specify any valid message flags. + /// + public static SearchQuery NotFlags (MessageFlags flags) + { + var list = new List (); + + if ((flags & MessageFlags.Seen) != 0) + list.Add (NotSeen); + if ((flags & MessageFlags.Answered) != 0) + list.Add (NotAnswered); + if ((flags & MessageFlags.Flagged) != 0) + list.Add (NotFlagged); + if ((flags & MessageFlags.Deleted) != 0) + list.Add (NotDeleted); + if ((flags & MessageFlags.Draft) != 0) + list.Add (NotDraft); + if ((flags & MessageFlags.Recent) != 0) + list.Add (NotRecent); + + if (list.Count == 0) + throw new ArgumentException ("No flags specified.", nameof (flags)); + + var query = list[0]; + for (int i = 1; i < list.Count; i++) + query = query.And (list[i]); + + return query; + } + + /// + /// Match messages that have the specified keyword set. + /// + /// + /// Matches messages that have the specified keyword set. + /// A keyword is a user-defined message flag. + /// + /// A . + /// The keyword. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery HasKeyword (string keyword) + { + if (keyword == null) + throw new ArgumentNullException (nameof (keyword)); + + if (keyword.Length == 0) + throw new ArgumentException ("The keyword cannot be an empty string.", nameof (keyword)); + + return new TextSearchQuery (SearchTerm.Keyword, keyword); + } + + /// + /// Match messages that have all of the specified keywords set. + /// + /// + /// Matches messages that have all of the specified keywords set. + /// A keyword is a user-defined message flag. + /// + /// A . + /// The keywords. + /// + /// is null. + /// + /// + /// One or more of the is null or empty. + /// -or- + /// No keywords were given. + /// + public static SearchQuery HasKeywords (IEnumerable keywords) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + var list = new List (); + + foreach (var keyword in keywords) { + if (string.IsNullOrEmpty (keyword)) + throw new ArgumentException ("Cannot search for null or empty keywords.", nameof (keywords)); + + list.Add (new TextSearchQuery (SearchTerm.Keyword, keyword)); + } + + if (list.Count == 0) + throw new ArgumentException ("No keywords specified.", nameof (keywords)); + + var query = list[0]; + for (int i = 1; i < list.Count; i++) + query = query.And (list[i]); + + return query; + } + + /// + /// Match messages that do not have the specified keyword set. + /// + /// + /// Matches messages that do not have the specified keyword set. + /// A keyword is a user-defined message flag. + /// + /// A . + /// The keyword. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery NotKeyword (string keyword) + { + if (keyword == null) + throw new ArgumentNullException (nameof (keyword)); + + if (keyword.Length == 0) + throw new ArgumentException ("The keyword cannot be an empty string.", nameof (keyword)); + + return new TextSearchQuery (SearchTerm.NotKeyword, keyword); + } + + /// + /// Match messages that do not have any of the specified keywords set. + /// + /// + /// Matches messages that do not have any of the specified keywords set. + /// A keyword is a user-defined message flag. + /// + /// A . + /// The keywords. + /// + /// is null. + /// + /// + /// One or more of the is null or empty. + /// -or- + /// No keywords were given. + /// + public static SearchQuery NotKeywords (IEnumerable keywords) + { + if (keywords == null) + throw new ArgumentNullException (nameof (keywords)); + + var list = new List (); + + foreach (var keyword in keywords) { + if (string.IsNullOrEmpty (keyword)) + throw new ArgumentException ("Cannot search for null or empty keywords.", nameof (keywords)); + + list.Add (new TextSearchQuery (SearchTerm.NotKeyword, keyword)); + } + + if (list.Count == 0) + throw new ArgumentException ("No flags specified.", nameof (keywords)); + + var query = list[0]; + for (int i = 1; i < list.Count; i++) + query = query.And (list[i]); + + return query; + } + + /// + /// Match messages where the specified header contains the specified text. + /// + /// + /// Matches messages where the specified header contains the specified text. + /// + /// A . + /// The header field to match against. + /// The text to match against. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + public static HeaderSearchQuery HeaderContains (string field, string text) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + if (field.Length == 0) + throw new ArgumentException ("Cannot search an empty header field name.", nameof (field)); + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + return new HeaderSearchQuery (field, text); + } + + /// + /// Match messages that are larger than the specified number of octets. + /// + /// + /// Matches messages that are larger than the specified number of octets. + /// + /// A . + /// The number of octets. + /// + /// is a negative value. + /// + public static NumericSearchQuery LargerThan (int octets) + { + if (octets < 0) + throw new ArgumentOutOfRangeException (nameof (octets)); + + return new NumericSearchQuery (SearchTerm.LargerThan, (ulong) octets); + } + + /// + /// Match messages where the raw message contains the specified text. + /// + /// + /// Matches messages where the raw message contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery MessageContains (string text) + { + return new TextSearchQuery (SearchTerm.MessageContains, text); + } + + /// + /// Match messages with the flag set but not the . + /// + /// + /// Matches messages with the flag set but not the . + /// + public static readonly SearchQuery New = new SearchQuery (SearchTerm.New); + + /// + /// Create a logical negation of the specified expression. + /// + /// + /// Creates a logical negation of the specified expression. + /// + /// A . + /// The expression + /// + /// is null. + /// + public static UnarySearchQuery Not (SearchQuery expr) + { + if (expr == null) + throw new ArgumentNullException (nameof (expr)); + + return new UnarySearchQuery (SearchTerm.Not, expr); + } + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotAnswered = new SearchQuery (SearchTerm.NotAnswered); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotDeleted = new SearchQuery (SearchTerm.NotDeleted); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotDraft = new SearchQuery (SearchTerm.NotDraft); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotFlagged = new SearchQuery (SearchTerm.NotFlagged); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotRecent = new SearchQuery (SearchTerm.NotRecent); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery NotSeen = new SearchQuery (SearchTerm.NotSeen); + + /// + /// Match messages that do not have the flag set. + /// + /// + /// Matches messages that do not have the flag set. + /// + public static readonly SearchQuery Old = new SearchQuery (SearchTerm.NotRecent); + + /// + /// Match messages older than the specified number of seconds. + /// + /// + /// Matches messages older than the specified number of seconds. + /// + /// A . + /// The number of seconds. + /// + /// The number of seconds cannot be less than 1. + /// + public static NumericSearchQuery OlderThan (int seconds) + { + if (seconds < 1) + throw new ArgumentOutOfRangeException (nameof (seconds)); + + return new NumericSearchQuery (SearchTerm.Older, (ulong) seconds); + } + + /// + /// Create a conditional OR operation. + /// + /// + /// A conditional OR operation only evaluates the second operand if the first operand evaluates to false. + /// + /// A representing the conditional OR operation. + /// The first operand. + /// The second operand. + /// + /// is null. + /// -or- + /// is null. + /// + public static BinarySearchQuery Or (SearchQuery left, SearchQuery right) + { + if (left == null) + throw new ArgumentNullException (nameof (left)); + + if (right == null) + throw new ArgumentNullException (nameof (right)); + + return new BinarySearchQuery (SearchTerm.Or, left, right); + } + + /// + /// Create a conditional OR operation. + /// + /// + /// A conditional OR operation only evaluates the second operand if the first operand evaluates to true. + /// + /// A representing the conditional AND operation. + /// An additional query to execute. + /// + /// is null. + /// + public BinarySearchQuery Or (SearchQuery expr) + { + if (expr == null) + throw new ArgumentNullException (nameof (expr)); + + return new BinarySearchQuery (SearchTerm.Or, this, expr); + } + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Recent = new SearchQuery (SearchTerm.Recent); + + /// + /// Match messages with the flag set. + /// + /// + /// Matches messages with the flag set. + /// + public static readonly SearchQuery Seen = new SearchQuery (SearchTerm.Seen); + + /// + /// Match messages that were sent on or after the specified date. + /// + /// + /// Matches messages that were sent on or after the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + [Obsolete ("Use SentSince (DateTime)")] + public static DateSearchQuery SentAfter (DateTime date) + { + return SentSince (date); + } + + /// + /// Match messages that were sent before the specified date. + /// + /// + /// Matches messages that were sent before the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery SentBefore (DateTime date) + { + return new DateSearchQuery (SearchTerm.SentBefore, date); + } + + /// + /// Match messages that were sent on the specified date. + /// + /// + /// Matches messages that were sent on the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery SentOn (DateTime date) + { + return new DateSearchQuery (SearchTerm.SentOn, date); + } + + /// + /// Match messages that were sent since the specified date. + /// + /// + /// Matches messages that were sent since the specified date. + /// The resolution of this search query does not include the time. + /// + /// A . + /// The date. + public static DateSearchQuery SentSince (DateTime date) + { + return new DateSearchQuery (SearchTerm.SentSince, date); + } + + /// + /// Match messages that are smaller than the specified number of octets. + /// + /// + /// Matches messages that are smaller than the specified number of octets. + /// + /// A . + /// The number of octets. + /// + /// is a negative value. + /// + public static NumericSearchQuery SmallerThan (int octets) + { + if (octets < 0) + throw new ArgumentOutOfRangeException (nameof (octets)); + + return new NumericSearchQuery (SearchTerm.SmallerThan, (ulong) octets); + } + + /// + /// Match messages where the Subject header contains the specified text. + /// + /// + /// Matches messages where the Subject header contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery SubjectContains (string text) + { + return new TextSearchQuery (SearchTerm.SubjectContains, text); + } + + /// + /// Match messages where the To header contains the specified text. + /// + /// + /// Matches messages where the To header contains the specified text. + /// + /// A . + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery ToContains (string text) + { + return new TextSearchQuery (SearchTerm.ToContains, text); + } + + /// + /// Limit the search query to messages with the specified unique identifiers. + /// + /// + /// Limits the search query to messages with the specified unique identifiers. + /// + /// A . + /// The unique identifiers. + /// + /// is null. + /// + /// + /// is empty. + /// + public static UidSearchQuery Uids (IList uids) + { + return new UidSearchQuery (uids); + } + + /// + /// Match messages younger than the specified number of seconds. + /// + /// + /// Matches messages younger than the specified number of seconds. + /// + /// A . + /// The number of seconds. + /// + /// The number of seconds cannot be less than 1. + /// + public static NumericSearchQuery YoungerThan (int seconds) + { + if (seconds < 1) + throw new ArgumentOutOfRangeException (nameof (seconds)); + + return new NumericSearchQuery (SearchTerm.Younger, (ulong) seconds); + } + + #region GMail extensions + + /// + /// Match messages that have the specified GMail message identifier. + /// + /// + /// This search term can only be used with GMail. + /// + /// A . + /// The GMail message identifier. + public static NumericSearchQuery GMailMessageId (ulong id) + { + return new NumericSearchQuery (SearchTerm.GMailMessageId, id); + } + + /// + /// Match messages belonging to the specified GMail thread. + /// + /// + /// This search term can only be used with GMail. + /// + /// A . + /// The GMail thread. + public static NumericSearchQuery GMailThreadId (ulong thread) + { + return new NumericSearchQuery (SearchTerm.GMailThreadId, thread); + } + + /// + /// Match messages that have the specified GMail label. + /// + /// + /// This search term can only be used with GMail. + /// + /// A . + /// The GMail label. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery HasGMailLabel (string label) + { + if (label == null) + throw new ArgumentNullException (nameof (label)); + + if (label.Length == 0) + throw new ArgumentException ("Cannot search for an empty string.", nameof (label)); + + return new TextSearchQuery (SearchTerm.GMailLabels, label); + } + + /// + /// Match messages using the GMail search expression. + /// + /// + /// This search term can only be used with GMail. + /// + /// A . + /// The raw GMail search text. + /// + /// is null. + /// + /// + /// is empty. + /// + public static TextSearchQuery GMailRawSearch (string expression) + { + if (expression == null) + throw new ArgumentNullException (nameof (expression)); + + if (expression.Length == 0) + throw new ArgumentException ("Cannot search for an empty string.", nameof (expression)); + + return new TextSearchQuery (SearchTerm.GMailRaw, expression); + } + + #endregion + + internal virtual SearchQuery Optimize (ISearchQueryOptimizer optimizer) + { + return optimizer.Reduce (this); + } + } +} diff --git a/src/MailKit/Search/SearchResults.cs b/src/MailKit/Search/SearchResults.cs new file mode 100644 index 0000000..d50a920 --- /dev/null +++ b/src/MailKit/Search/SearchResults.cs @@ -0,0 +1,116 @@ +// +// SearchResults.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MailKit.Search { + /// + /// The results of a search. + /// + /// + /// The results of a search. + /// + public class SearchResults + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The sort-order to use for the unique identifiers. + public SearchResults (SortOrder order = SortOrder.None) + { + UniqueIds = new UniqueIdSet (order); + } + + /// + /// Get or set the unique identifiers of the messages that matched the search query. + /// + /// + /// Gets or sets the unique identifiers of the messages that matched the search query. + /// + /// The unique identifiers. + public IList UniqueIds { + get; set; + } + + /// + /// Get or set the number of messages that matched the search query. + /// + /// + /// Gets or sets the number of messages that matched the search query. + /// + /// The count. + public int Count { + get; set; + } + + /// + /// Get or set the minimum unique identifier that matched the search query. + /// + /// + /// Gets or sets the minimum unique identifier that matched the search query. + /// + /// The minimum unique identifier. + public UniqueId? Min { + get; set; + } + + /// + /// Get or set the maximum unique identifier that matched the search query. + /// + /// + /// Gets or sets the maximum unique identifier that matched the search query. + /// + /// The maximum unique identifier. + public UniqueId? Max { + get; set; + } + + /// + /// Gets or sets the mod-sequence identifier of the messages that matched the search query. + /// + /// + /// Gets or sets the mod-sequence identifier of the messages that matched the search query. + /// + /// The mod-sequence identifier. + public ulong? ModSeq { + get; set; + } + + /// + /// Gets or sets the relevancy scores of the messages that matched the search query. + /// + /// + /// Gets or sets the relevancy scores of the messages that matched the search query. + /// + /// The relevancy scores. + public IList Relevancy { + get; set; + } + } +} diff --git a/src/MailKit/Search/SearchTerm.cs b/src/MailKit/Search/SearchTerm.cs new file mode 100644 index 0000000..1a12f74 --- /dev/null +++ b/src/MailKit/Search/SearchTerm.cs @@ -0,0 +1,288 @@ +// +// SearchTerm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Search { + /// + /// A search term. + /// + /// + /// The search term as used by . + /// + public enum SearchTerm { + /// + /// A search term that matches all messages. + /// + All, + + /// + /// A search term that logically combines 2 or more other + /// search expressions such that messages must match both + /// expressions. + /// + And, + + /// + /// A search term that matches messages that have the specified annotation. + /// + Annotation, + + /// + /// A search term that matches answered messages. + /// + Answered, + + /// + /// A search term that matches messages that contain a specified + /// string within the Bcc header. + /// + BccContains, + + /// + /// A search term that matches messages that contain a specified + /// string within the body of the message. + /// + BodyContains, + + /// + /// A search term that matches messages that contain a specified + /// string within the Cc header. + /// + CcContains, + + /// + /// A search term that matches deleted messages. + /// + Deleted, + + /// + /// A search term that matches messages delivered after a specified date. + /// + DeliveredAfter, + + /// + /// A search term that matches messages delivered before a specified date. + /// + DeliveredBefore, + + /// + /// A search term that matches messages delivered on a specified date. + /// + DeliveredOn, + + /// + /// A search term that matches draft messages. + /// + Draft, + + /// + /// A search term that makes use of a predefined filter. + /// + Filter, + + /// + /// A search term that matches flagged messages. + /// + Flagged, + + /// + /// A search term that matches messages that contain a specified + /// string within the From header. + /// + FromContains, + + /// + /// A search term that modifies another search expression to allow + /// fuzzy matching. + /// + Fuzzy, + + /// + /// A search term that matches messages that contain a specified + /// string within a particular header. + /// + HeaderContains, + + /// + /// A search term that matches messages that contain a specified + /// keyword. + /// + Keyword, + + /// + /// A search term that matches messages that are larger than a + /// specified number of bytes. + /// + LargerThan, + + /// + /// A search term that matches messages that contain a specified + /// string anywhere within the message. + /// + MessageContains, + + /// + /// A search term that matches messages that have the specified + /// modification sequence value. + /// + ModSeq, + + /// + /// A search term that matches new messages. + /// + New, + + /// + /// A search term that modifies another search expression such that + /// messages must match the logical inverse of the expression. + /// + Not, + + /// + /// A search term that matches messages that have not been answered. + /// + NotAnswered, + + /// + /// A search term that matches messages that have not been deleted. + /// + NotDeleted, + + /// + /// A search term that matches messages that are not drafts. + /// + NotDraft, + + /// + /// A search term that matches messages that have not been flagged. + /// + NotFlagged, + + /// + /// A search term that matches messages that do not contain a specified + /// keyword. + /// + NotKeyword, + + /// + /// A search term that matches messages that are not recent. + /// + NotRecent, + + /// + /// A search term that matches messages that have not been seen. + /// + NotSeen, + + /// + /// A search term that matches messages that are older than a specified date. + /// + Older, + + /// + /// A search term that logically combines 2 or more other + /// search expressions such that messages only need to match + /// one of the expressions. + /// + Or, + + /// + /// A search term that matches messages that are recent. + /// + Recent, + + /// + /// A search term that matches messages that have been seen. + /// + Seen, + + /// + /// A search term that matches messages that were sent before a specified date. + /// + SentBefore, + + /// + /// A search term that matches messages that were sent on a specified date. + /// + SentOn, + + /// + /// A search term that matches messages that were sent since a specified date. + /// + SentSince, + + /// + /// A search term that matches messages that are smaller than a + /// specified number of bytes. + /// + SmallerThan, + + /// + /// A search term that matches messages that contain a specified + /// string within the Subject header. + /// + SubjectContains, + + /// + /// A search term that matches messages that contain a specified + /// string within the To header. + /// + ToContains, + + /// + /// A search term that matches messages included within a specified + /// set of unique identifiers. + /// + Uid, + + /// + /// A search term that matches messages that are younger than a specified date. + /// + Younger, + + // GMail SEARCH extensions + + /// + /// A search term that matches messages with a specified GMail message identifier. + /// + GMailMessageId, + + /// + /// A search term that matches messages with a specified GMail thread (conversation) + /// identifier. + /// + GMailThreadId, + + /// + /// A search term that matches messages with the specified GMail labels. + /// + GMailLabels, + + /// + /// A search term that uses the GMail search syntax. + /// + GMailRaw, + } +} diff --git a/src/MailKit/Search/SortOrder.cs b/src/MailKit/Search/SortOrder.cs new file mode 100644 index 0000000..d07f4f8 --- /dev/null +++ b/src/MailKit/Search/SortOrder.cs @@ -0,0 +1,50 @@ +// +// SortOrder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Search { + /// + /// An enumeration of sort orders. + /// + /// + /// An enumeration of sort orders. + /// + public enum SortOrder { + /// + /// No sorting order. + /// + None, + + /// + /// Sort in ascending order. + /// + Ascending, + + /// + /// Sort in descending order. + /// + Descending + } +} diff --git a/src/MailKit/Search/TextSearchQuery.cs b/src/MailKit/Search/TextSearchQuery.cs new file mode 100644 index 0000000..2bc2812 --- /dev/null +++ b/src/MailKit/Search/TextSearchQuery.cs @@ -0,0 +1,75 @@ +// +// TextSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search +{ + /// + /// A text-based search query. + /// + /// + /// A text-based search query. + /// + public class TextSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new text-based search query. + /// + /// The search term. + /// The text to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public TextSearchQuery (SearchTerm term, string text) : base (term) + { + if (text == null) + throw new ArgumentNullException (nameof (text)); + + if (text.Length == 0) + throw new ArgumentException ("Cannot search for an empty string.", nameof (text)); + + Text = text; + } + + /// + /// Gets the text to match against. + /// + /// + /// Gets the text to match against. + /// + /// The text. + public string Text { + get; private set; + } + } +} diff --git a/src/MailKit/Search/UidSearchQuery.cs b/src/MailKit/Search/UidSearchQuery.cs new file mode 100644 index 0000000..6dee25b --- /dev/null +++ b/src/MailKit/Search/UidSearchQuery.cs @@ -0,0 +1,94 @@ +// +// UidSearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit.Search +{ + /// + /// A unique identifier-based search query. + /// + /// + /// A unique identifier-based search query. + /// + public class UidSearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new unique identifier-based search query. + /// + /// The unique identifiers to match against. + /// + /// is null. + /// + /// + /// is empty. + /// + public UidSearchQuery (IList uids) : base (SearchTerm.Uid) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (uids.Count == 0) + throw new ArgumentException ("Cannot search for an empty set of unique identifiers.", nameof (uids)); + + Uids = uids; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new unique identifier-based search query. + /// + /// The unique identifier to match against. + /// + /// is an invalid unique identifier. + /// + public UidSearchQuery (UniqueId uid) : base (SearchTerm.Uid) + { + if (!uid.IsValid) + throw new ArgumentException ("Cannot search for an invalid unique identifier.", nameof (uid)); + + Uids = new UniqueIdSet (SortOrder.Ascending); + Uids.Add (uid); + } + + /// + /// Gets the unique identifiers to match against. + /// + /// + /// Gets the unique identifiers to match against. + /// + /// The unique identifiers. + public new IList Uids { + get; private set; + } + } +} diff --git a/src/MailKit/Search/UnarySearchQuery.cs b/src/MailKit/Search/UnarySearchQuery.cs new file mode 100644 index 0000000..667bf53 --- /dev/null +++ b/src/MailKit/Search/UnarySearchQuery.cs @@ -0,0 +1,82 @@ +// +// UnarySearchQuery.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Search +{ + /// + /// A unary search query such as a NOT expression. + /// + /// + /// A unary search query such as a NOT expression. + /// + public class UnarySearchQuery : SearchQuery + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new unary search query. + /// + /// The search term. + /// The operand. + /// + /// is null. + /// + public UnarySearchQuery (SearchTerm term, SearchQuery operand) : base (term) + { + if (operand == null) + throw new ArgumentNullException (nameof (operand)); + + Operand = operand; + } + + /// + /// Gets the inner operand. + /// + /// + /// Gets the inner operand. + /// + /// The operand. + public SearchQuery Operand { + get; private set; + } + + internal override SearchQuery Optimize (ISearchQueryOptimizer optimizer) + { + var operand = Operand.Optimize (optimizer); + SearchQuery unary; + + if (operand != Operand) + unary = new UnarySearchQuery (Term, operand); + else + unary = this; + + return optimizer.Reduce (unary); + } + } +} diff --git a/src/MailKit/Security/AuthenticationException.cs b/src/MailKit/Security/AuthenticationException.cs new file mode 100644 index 0000000..357b9d2 --- /dev/null +++ b/src/MailKit/Security/AuthenticationException.cs @@ -0,0 +1,94 @@ +// +// AuthenticationException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Runtime.Serialization; +#endif + +namespace MailKit.Security { + /// + /// The exception that is thrown when there is an authentication error. + /// + /// + /// The exception that is thrown when there is an authentication error. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class AuthenticationException : Exception + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the seriaized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + protected AuthenticationException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public AuthenticationException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public AuthenticationException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public AuthenticationException () : base ("Authentication failed.") + { + } + } +} diff --git a/src/MailKit/Security/KeyedHashAlgorithm.cs b/src/MailKit/Security/KeyedHashAlgorithm.cs new file mode 100644 index 0000000..97e46f2 --- /dev/null +++ b/src/MailKit/Security/KeyedHashAlgorithm.cs @@ -0,0 +1,137 @@ +// +// KeyedHashAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using Windows.Storage.Streams; +using Windows.Security.Cryptography; +using Windows.Security.Cryptography.Core; + +namespace MailKit.Security { + /// + /// A keyed hash algorithm. + /// + /// + /// A keyed hash algorithm. + /// + public abstract class KeyedHashAlgorithm : IDisposable + { + CryptographicHash hmac; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new keyed hash algorithm context. + /// + /// The MAC algorithm name. + /// The secret key. + protected KeyedHashAlgorithm (string algorithm, byte[] key) + { + var mac = MacAlgorithmProvider.OpenAlgorithm (algorithm); + var buf = CryptographicBuffer.CreateFromByteArray (key); + hmac = mac.CreateHash (buf); + } + + /// + /// Computes the hash code for the buffer. + /// + /// + /// Computes the hash code for the buffer. + /// + /// The computed hash code. + /// The buffer. + /// + /// is null. + /// + /// + /// The keyed hash algorithm context has been disposed. + /// + public byte[] ComputeHash (byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException ("data"); + + hmac.Append (CryptographicBuffer.CreateFromByteArray (buffer)); + var value = hmac.GetValueAndReset (); + byte[] hash; + + CryptographicBuffer.CopyToByteArray (value, out hash); + + return hash; + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After calling + /// , you must release all references to the so the + /// garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + } + } + + /// + /// The HMAC SHA-1 algorithm. + /// + /// + /// The HMAC SHA-1 algorithm. + /// + public class HMACSHA1 : KeyedHashAlgorithm + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new HMAC SHA-1 context. + /// + /// The secret key. + public HMACSHA1 (byte[] key) : base (MacAlgorithmNames.HmacSha1, key) + { + } + } + + /// + /// The HMAC SHA-256 algorithm. + /// + /// + /// The HMAC SHA-256 algorithm. + /// + public class HMACSHA256 : KeyedHashAlgorithm + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new HMAC SHA-256 context. + /// + /// The secret key. + public HMACSHA256 (byte[] key) : base (MacAlgorithmNames.HmacSha256, key) + { + } + } +} diff --git a/src/MailKit/Security/Ntlm/BitConverterLE.cs b/src/MailKit/Security/Ntlm/BitConverterLE.cs new file mode 100644 index 0000000..4db415b --- /dev/null +++ b/src/MailKit/Security/Ntlm/BitConverterLE.cs @@ -0,0 +1,112 @@ +// +// Mono.Security.BitConverterLE.cs +// Like System.BitConverter but always little endian +// +// Author: Bernie Solomon +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace MailKit.Security.Ntlm +{ + sealed class BitConverterLE + { + BitConverterLE () + { + } + + unsafe static byte[] GetULongBytes (byte *bytes) + { + if (BitConverter.IsLittleEndian) + return new [] { bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7] }; + + return new [] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0] }; + } + + unsafe internal static byte[] GetBytes (long value) + { + return GetULongBytes ((byte *) &value); + } + + unsafe static void UShortFromBytes (byte *dst, byte[] src, int startIndex) + { + if (BitConverter.IsLittleEndian) { + dst[0] = src[startIndex]; + dst[1] = src[startIndex + 1]; + } else { + dst[0] = src[startIndex + 1]; + dst[1] = src[startIndex]; + } + } + + unsafe static void UIntFromBytes (byte *dst, byte[] src, int startIndex) + { + if (BitConverter.IsLittleEndian) { + dst[0] = src[startIndex]; + dst[1] = src[startIndex + 1]; + dst[2] = src[startIndex + 2]; + dst[3] = src[startIndex + 3]; + } else { + dst[0] = src[startIndex + 3]; + dst[1] = src[startIndex + 2]; + dst[2] = src[startIndex + 1]; + dst[3] = src[startIndex]; + } + } + + unsafe internal static short ToInt16 (byte[] value, int startIndex) + { + short ret; + + UShortFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static int ToInt32 (byte[] value, int startIndex) + { + int ret; + + UIntFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static ushort ToUInt16 (byte[] value, int startIndex) + { + ushort ret; + + UShortFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + + unsafe internal static uint ToUInt32 (byte[] value, int startIndex) + { + uint ret; + + UIntFromBytes ((byte *) &ret, value, startIndex); + + return ret; + } + } +} diff --git a/src/MailKit/Security/Ntlm/ChallengeResponse2.cs b/src/MailKit/Security/Ntlm/ChallengeResponse2.cs new file mode 100644 index 0000000..06832c9 --- /dev/null +++ b/src/MailKit/Security/Ntlm/ChallengeResponse2.cs @@ -0,0 +1,280 @@ +// +// Mono.Security.Protocol.Ntlm.ChallengeResponse +// Implements Challenge Response for NTLM v1 and NTLM v2 Session +// +// Authors: Sebastien Pouliot +// Martin Baulig +// Jeffrey Stedfast +// +// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (c) 2004 Novell (http://www.novell.com) +// Copyright (c) 2012 Xamarin, Inc. (http://www.xamarin.com) +// +// References +// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär +// http://www.innovation.ch/java/ntlm.html +// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass +// http://davenport.sourceforge.net/ntlm.html +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Text; +using System.Security.Cryptography; + +namespace MailKit.Security.Ntlm { + static class ChallengeResponse2 + { + static readonly byte[] Magic = { 0x4B, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; + + // This is the pre-encrypted magic value with a null DES key (0xAAD3B435B51404EE) + // Ref: http://packetstormsecurity.nl/Crackers/NT/l0phtcrack/l0phtcrack2.5-readme.html + static readonly byte[] NullEncMagic = { 0xAA, 0xD3, 0xB4, 0x35, 0xB5, 0x14, 0x04, 0xEE }; + + static byte[] ComputeLM (string password, byte[] challenge) + { + var buffer = new byte[21]; + + // create Lan Manager password + using (var des = DES.Create ()) { + des.Mode = CipherMode.ECB; + + // Note: In .NET DES cannot accept a weak key + // this can happen for a null password + if (string.IsNullOrEmpty (password)) { + Buffer.BlockCopy (NullEncMagic, 0, buffer, 0, 8); + } else { + des.Key = PasswordToKey (password, 0); + using (var ct = des.CreateEncryptor ()) + ct.TransformBlock (Magic, 0, 8, buffer, 0); + } + + // and if a password has less than 8 characters + if (password == null || password.Length < 8) { + Buffer.BlockCopy (NullEncMagic, 0, buffer, 8, 8); + } else { + des.Key = PasswordToKey (password, 7); + using (var ct = des.CreateEncryptor ()) + ct.TransformBlock (Magic, 0, 8, buffer, 8); + } + } + + return GetResponse (challenge, buffer); + } + + static byte[] ComputeNtlmPassword (string password) + { + var buffer = new byte[21]; + + // create NT password + using (var md4 = new MD4 ()) { + var data = password == null ? new byte[0] : Encoding.Unicode.GetBytes (password); + var hash = md4.ComputeHash (data); + Buffer.BlockCopy (hash, 0, buffer, 0, 16); + + // clean up + Array.Clear (data, 0, data.Length); + Array.Clear (hash, 0, hash.Length); + } + + return buffer; + } + + static byte[] ComputeNtlm (string password, byte[] challenge) + { + var buffer = ComputeNtlmPassword (password); + return GetResponse (challenge, buffer); + } + + static void ComputeNtlmV2Session (string password, byte[] challenge, out byte[] lm, out byte[] ntlm) + { + var nonce = new byte[8]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (nonce); + + var sessionNonce = new byte[challenge.Length + 8]; + challenge.CopyTo (sessionNonce, 0); + nonce.CopyTo (sessionNonce, challenge.Length); + + lm = new byte[24]; + nonce.CopyTo (lm, 0); + + using (var md5 = MD5.Create ()) { + var hash = md5.ComputeHash (sessionNonce); + var newChallenge = new byte[8]; + + Array.Copy (hash, newChallenge, 8); + + ntlm = ComputeNtlm (password, newChallenge); + + // clean up + Array.Clear (newChallenge, 0, newChallenge.Length); + Array.Clear (hash, 0, hash.Length); + } + + // clean up + Array.Clear (sessionNonce, 0, sessionNonce.Length); + Array.Clear (nonce, 0, nonce.Length); + } + + static byte[] ComputeNtlmV2 (Type2Message type2, string username, string password, string domain) + { + var ntlm_hash = ComputeNtlmPassword (password); + + var ubytes = Encoding.Unicode.GetBytes (username.ToUpperInvariant ()); + var tbytes = Encoding.Unicode.GetBytes (domain); + + var bytes = new byte[ubytes.Length + tbytes.Length]; + ubytes.CopyTo (bytes, 0); + Array.Copy (tbytes, 0, bytes, ubytes.Length, tbytes.Length); + + byte[] ntlm_v2_hash; + + using (var md5 = new HMACMD5 (ntlm_hash)) + ntlm_v2_hash = md5.ComputeHash (bytes); + + Array.Clear (ntlm_hash, 0, ntlm_hash.Length); + + using (var md5 = new HMACMD5 (ntlm_v2_hash)) { + var timestamp = DateTime.Now.Ticks - 504911232000000000; + var nonce = new byte[8]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (nonce); + + var targetInfo = type2.EncodedTargetInfo; + var blob = new byte[28 + targetInfo.Length]; + blob[0] = 0x01; + blob[1] = 0x01; + + Buffer.BlockCopy (BitConverterLE.GetBytes (timestamp), 0, blob, 8, 8); + + Buffer.BlockCopy (nonce, 0, blob, 16, 8); + Buffer.BlockCopy (targetInfo, 0, blob, 28, targetInfo.Length); + + var challenge = type2.Nonce; + + var hashInput = new byte[challenge.Length + blob.Length]; + challenge.CopyTo (hashInput, 0); + blob.CopyTo (hashInput, challenge.Length); + + var blobHash = md5.ComputeHash (hashInput); + + var response = new byte[blob.Length + blobHash.Length]; + blobHash.CopyTo (response, 0); + blob.CopyTo (response, blobHash.Length); + + Array.Clear (ntlm_v2_hash, 0, ntlm_v2_hash.Length); + Array.Clear (hashInput, 0, hashInput.Length); + Array.Clear (blobHash, 0, blobHash.Length); + Array.Clear (nonce, 0, nonce.Length); + Array.Clear (blob, 0, blob.Length); + + return response; + } + } + + public static void Compute (Type2Message type2, NtlmAuthLevel level, string username, string password, string domain, out byte[] lm, out byte[] ntlm) + { + lm = null; + + switch (level) { + case NtlmAuthLevel.LM_and_NTLM: + lm = ComputeLM (password, type2.Nonce); + ntlm = ComputeNtlm (password, type2.Nonce); + break; + case NtlmAuthLevel.LM_and_NTLM_and_try_NTLMv2_Session: + if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) == 0) + goto case NtlmAuthLevel.LM_and_NTLM; + ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); + break; + case NtlmAuthLevel.NTLM_only: + if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) + ComputeNtlmV2Session (password, type2.Nonce, out lm, out ntlm); + else + ntlm = ComputeNtlm (password, type2.Nonce); + break; + case NtlmAuthLevel.NTLMv2_only: + ntlm = ComputeNtlmV2 (type2, username, password, domain); + break; + default: + throw new InvalidOperationException (); + } + } + + static byte[] GetResponse (byte[] challenge, byte[] pwd) + { + var response = new byte[24]; + + using (var des = DES.Create ()) { + des.Mode = CipherMode.ECB; + des.Key = PrepareDESKey (pwd, 0); + + using (var ct = des.CreateEncryptor ()) + ct.TransformBlock (challenge, 0, 8, response, 0); + + des.Key = PrepareDESKey (pwd, 7); + + using (var ct = des.CreateEncryptor ()) + ct.TransformBlock (challenge, 0, 8, response, 8); + + des.Key = PrepareDESKey (pwd, 14); + + using (var ct = des.CreateEncryptor ()) + ct.TransformBlock (challenge, 0, 8, response, 16); + } + + return response; + } + + static byte[] PrepareDESKey (byte[] key56bits, int position) + { + // convert to 8 bytes + var key = new byte[8]; + + key[0] = key56bits [position]; + key[1] = (byte) ((key56bits[position] << 7) | (key56bits[position + 1] >> 1)); + key[2] = (byte) ((key56bits[position + 1] << 6) | (key56bits[position + 2] >> 2)); + key[3] = (byte) ((key56bits[position + 2] << 5) | (key56bits[position + 3] >> 3)); + key[4] = (byte) ((key56bits[position + 3] << 4) | (key56bits[position + 4] >> 4)); + key[5] = (byte) ((key56bits[position + 4] << 3) | (key56bits[position + 5] >> 5)); + key[6] = (byte) ((key56bits[position + 5] << 2) | (key56bits[position + 6] >> 6)); + key[7] = (byte) (key56bits[position + 6] << 1); + + return key; + } + + static byte[] PasswordToKey (string password, int position) + { + int len = Math.Min (password.Length - position, 7); + var key7 = new byte[7]; + + Encoding.ASCII.GetBytes (password.ToUpper (), position, len, key7, 0); + var key8 = PrepareDESKey (key7, 0); + + // cleanup intermediate key material + Array.Clear (key7, 0, key7.Length); + + return key8; + } + } +} diff --git a/src/MailKit/Security/Ntlm/DES.cs b/src/MailKit/Security/Ntlm/DES.cs new file mode 100644 index 0000000..9297fbb --- /dev/null +++ b/src/MailKit/Security/Ntlm/DES.cs @@ -0,0 +1,232 @@ +// +// DES.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MailKit.Security.Ntlm { + class DES : SymmetricAlgorithm + { + DES () + { + BlockSize = 64; + KeySize = 64; + } + + public static DES Create () + { + return new DES (); + } + + public override KeySizes[] LegalBlockSizes { + get { return new [] { new KeySizes (64, 64, 0) }; } + } + + public override KeySizes[] LegalKeySizes { + get { return new [] { new KeySizes (64, 64, 0) }; } + } + + public override void GenerateIV () + { + var iv = new byte[8]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (iv); + + IV = iv; + } + + public override void GenerateKey () + { + var key = new byte[8]; + + using (var rng = RandomNumberGenerator.Create ()) { + do { + rng.GetBytes (key); + } while (IsWeakKey (key) || IsSemiWeakKey (key)); + } + + Key = key; + } + + class DesTransform : ICryptoTransform + { + readonly DesEngine engine; + + public DesTransform (bool encryption, byte[] key) + { + engine = new DesEngine (); + + engine.Init (encryption, new KeyParameter (key)); + } + + public bool CanReuseTransform { + get { return false; } + } + + public bool CanTransformMultipleBlocks { + get { return false; } + } + + public int InputBlockSize { + get { return 8; } + } + + public int OutputBlockSize { + get { return 8; } + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (inputBuffer == null) + throw new ArgumentNullException ("inputBuffer"); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException ("inputOffset"); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException ("inputCount"); + + if (inputCount != 8) + throw new ArgumentOutOfRangeException ("inputCount", "Can only transform 8 bytes at a time."); + + if (outputBuffer == null) + throw new ArgumentNullException ("outputBuffer"); + + if (outputOffset < 0 || outputOffset > outputBuffer.Length - 8) + throw new ArgumentOutOfRangeException ("outputOffset"); + + return engine.ProcessBlock (inputBuffer, inputOffset, outputBuffer, outputOffset); + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputBuffer == null) + throw new ArgumentNullException ("inputBuffer"); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException ("inputOffset"); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException ("inputCount"); + + var output = new byte[8]; + + engine.ProcessBlock (inputBuffer, inputOffset, output, 0); + + return output; + } + + public void Dispose () + { + } + } + + public override ICryptoTransform CreateDecryptor (byte[] rgbKey, byte[] rgbIV) + { + return new DesTransform (false, rgbKey); + } + + public override ICryptoTransform CreateEncryptor (byte[] rgbKey, byte[] rgbIV) + { + return new DesTransform (true, rgbKey); + } + + // The following code is Copyright (C) Microsoft Corporation. All rights reserved. + + public static bool IsWeakKey (byte[] rgbKey) + { + if (!IsLegalKeySize (rgbKey)) + throw new CryptographicException ("Invalid key size."); + + byte[] rgbOddParityKey = FixupKeyParity (rgbKey); + ulong key = QuadWordFromBigEndian (rgbOddParityKey); + + return ((key == 0x0101010101010101) || + (key == 0xfefefefefefefefe) || + (key == 0x1f1f1f1f0e0e0e0e) || + (key == 0xe0e0e0e0f1f1f1f1)); + } + + public static bool IsSemiWeakKey (byte[] rgbKey) + { + if (!IsLegalKeySize (rgbKey)) + throw new CryptographicException ("Invalid key size."); + + byte[] rgbOddParityKey = FixupKeyParity (rgbKey); + ulong key = QuadWordFromBigEndian (rgbOddParityKey); + + return ((key == 0x01fe01fe01fe01fe) || + (key == 0xfe01fe01fe01fe01) || + (key == 0x1fe01fe00ef10ef1) || + (key == 0xe01fe01ff10ef10e) || + (key == 0x01e001e001f101f1) || + (key == 0xe001e001f101f101) || + (key == 0x1ffe1ffe0efe0efe) || + (key == 0xfe1ffe1ffe0efe0e) || + (key == 0x011f011f010e010e) || + (key == 0x1f011f010e010e01) || + (key == 0xe0fee0fef1fef1fe) || + (key == 0xfee0fee0fef1fef1)); + } + + static byte[] FixupKeyParity (byte[] key) + { + byte[] oddParityKey = new byte[key.Length]; + + for (int index = 0; index < key.Length; index++) { + // Get the bits we are interested in + oddParityKey[index] = (byte) (key[index] & 0xfe); + // Get the parity of the sum of the previous bits + byte tmp1 = (byte) ((oddParityKey[index] & 0xF) ^ (oddParityKey[index] >> 4)); + byte tmp2 = (byte) ((tmp1 & 0x3) ^ (tmp1 >> 2)); + byte sumBitsMod2 = (byte) ((tmp2 & 0x1) ^ (tmp2 >> 1)); + // We need to set the last bit in oddParityKey[index] to the negation + // of the last bit in sumBitsMod2 + if (sumBitsMod2 == 0) + oddParityKey[index] |= 1; + } + + return oddParityKey; + } + + static bool IsLegalKeySize (byte[] rgbKey) + { + return rgbKey != null && rgbKey.Length == 8; + } + + static ulong QuadWordFromBigEndian (byte[] block) + { + return (((ulong) block[0]) << 56) | (((ulong) block[1]) << 48) | + (((ulong) block[2]) << 40) | (((ulong) block[3]) << 32) | + (((ulong) block[4]) << 24) | (((ulong) block[5]) << 16) | + (((ulong) block[6]) << 8) | ((ulong) block[7]); + } + } +} diff --git a/src/MailKit/Security/Ntlm/HMACMD5.cs b/src/MailKit/Security/Ntlm/HMACMD5.cs new file mode 100644 index 0000000..ea88e8e --- /dev/null +++ b/src/MailKit/Security/Ntlm/HMACMD5.cs @@ -0,0 +1,202 @@ +// +// HMACMD5.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MailKit.Security.Ntlm { + class HMACMD5 : IDisposable + { + readonly HMac hash = new HMac (new MD5Digest ()); + byte[] hashValue, key; + bool disposed; + + public HMACMD5 (byte[] key) + { + Key = key; + } + + ~HMACMD5 () + { + Dispose (false); + } + + public byte[] Hash + { + get { + if (hashValue == null) + throw new InvalidOperationException ("No hash value computed."); + + return hashValue; + } + } + + public byte[] Key { + get { return key; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (key != null) + Array.Clear (key, 0, key.Length); + + key = value; + Initialize (); + } + } + + void HashCore (byte[] block, int offset, int size) + { + hash.BlockUpdate (block, offset, size); + } + + byte[] HashFinal () + { + var value = new byte[hash.GetMacSize ()]; + + hash.DoFinal (value, 0); + hash.Reset (); + + return value; + } + + public void Initialize () + { + hash.Init (new KeyParameter (Key)); + } + + public void Clear () + { + Dispose (false); + } + + public byte[] ComputeHash (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || offset > buffer.Length - count) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (disposed) + throw new ObjectDisposedException ("HashAlgorithm"); + + HashCore (buffer, offset, count); + hashValue = HashFinal (); + + return hashValue; + } + + public byte[] ComputeHash (byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + return ComputeHash (buffer, 0, buffer.Length); + } + + public byte[] ComputeHash (Stream inputStream) + { + // don't read stream unless object is ready to use + if (disposed) + throw new ObjectDisposedException ("HashAlgorithm"); + + var buffer = new byte[4096]; + int nread; + + do { + if ((nread = inputStream.Read (buffer, 0, buffer.Length)) > 0) + HashCore (buffer, 0, nread); + } while (nread > 0); + + hashValue = HashFinal (); + + return hashValue; + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (inputBuffer == null) + throw new ArgumentNullException (nameof (inputBuffer)); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException (nameof (inputOffset)); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + + if (outputBuffer != null) { + if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (outputOffset)); + } + + HashCore (inputBuffer, inputOffset, inputCount); + + if (outputBuffer != null) + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + + return inputCount; + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputCount < 0) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + + var outputBuffer = new byte[inputCount]; + + // note: other exceptions are handled by Buffer.BlockCopy + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, 0, inputCount); + + HashCore (inputBuffer, inputOffset, inputCount); + hashValue = HashFinal (); + + return outputBuffer; + } + + void Dispose (bool disposing) + { + if (key != null) { + Array.Clear (key, 0, Key.Length); + key = null; + } + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + disposed = true; + } + } +} diff --git a/src/MailKit/Security/Ntlm/MD4.cs b/src/MailKit/Security/Ntlm/MD4.cs new file mode 100644 index 0000000..2f8ab7b --- /dev/null +++ b/src/MailKit/Security/Ntlm/MD4.cs @@ -0,0 +1,410 @@ +// +// MD4.cs +// +// Authors: Sebastien Pouliot +// Jeffrey Stedfast +// +// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (c) 2004-2005, 2010 Novell, Inc (http://www.novell.com) +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; + +namespace MailKit.Security.Ntlm { + sealed class MD4 : IDisposable + { + const int S11 = 3; + const int S12 = 7; + const int S13 = 11; + const int S14 = 19; + const int S21 = 3; + const int S22 = 5; + const int S23 = 9; + const int S24 = 13; + const int S31 = 3; + const int S32 = 9; + const int S33 = 11; + const int S34 = 15; + + bool disposed; + byte[] hashValue; + byte[] buffered; + uint[] state; + uint[] count; + uint[] x; + + public MD4 () + { + // we allocate the context memory + buffered = new byte[64]; + state = new uint[4]; + count = new uint[2]; + + // temporary buffer in MD4Transform that we don't want to allocate on each iteration + x = new uint[16]; + + // the initialize our context + Initialize (); + } + + ~MD4 () + { + Dispose (false); + } + + public byte[] Hash { + get { + if (hashValue == null) + throw new InvalidOperationException ("No hash value computed."); + + return hashValue; + } + } + + void HashCore (byte[] block, int offset, int size) + { + // Compute number of bytes mod 64 + int index = (int) ((count[0] >> 3) & 0x3F); + + // Update number of bits + count[0] += (uint) (size << 3); + if (count[0] < (size << 3)) + count[1]++; + + count[1] += (uint) (size >> 29); + + int partLen = 64 - index; + int i = 0; + + // Transform as many times as possible. + if (size >= partLen) { + Buffer.BlockCopy (block, offset, buffered, index, partLen); + MD4Transform (buffered, 0); + + for (i = partLen; i + 63 < size; i += 64) + MD4Transform (block, offset + i); + + index = 0; + } + + // Buffer remaining input + Buffer.BlockCopy (block, offset + i, buffered, index, size - i); + } + + byte[] HashFinal () + { + // Save number of bits + var bits = new byte[8]; + Encode (bits, count); + + // Pad out to 56 mod 64. + uint index = ((count [0] >> 3) & 0x3f); + int padLen = (int) ((index < 56) ? (56 - index) : (120 - index)); + HashCore (Padding (padLen), 0, padLen); + + // Append length (before padding) + HashCore (bits, 0, 8); + + // Store state in digest + var digest = new byte[16]; + Encode (digest, state); + + return digest; + } + + public void Initialize () + { + count[0] = 0; + count[1] = 0; + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + + // Clear sensitive information + Array.Clear (buffered, 0, 64); + Array.Clear (x, 0, 16); + } + + static byte[] Padding (int length) + { + if (length > 0) { + var padding = new byte[length]; + padding[0] = 0x80; + return padding; + } + + return null; + } + + // F, G and H are basic MD4 functions. + static uint F (uint x, uint y, uint z) + { + return (x & y) | (~x & z); + } + + static uint G (uint x, uint y, uint z) + { + return (x & y) | (x & z) | (y & z); + } + + static uint H (uint x, uint y, uint z) + { + return x ^ y ^ z; + } + + // ROTATE_LEFT rotates x left n bits. + static uint ROL (uint x, byte n) + { + return (x << n) | (x >> (32 - n)); + } + + /* FF, GG and HH are transformations for rounds 1, 2 and 3 */ + /* Rotation is separate from addition to prevent recomputation */ + static void FF (ref uint a, uint b, uint c, uint d, uint x, byte s) + { + a += F (b, c, d) + x; + a = ROL (a, s); + } + + static void GG (ref uint a, uint b, uint c, uint d, uint x, byte s) + { + a += G (b, c, d) + x + 0x5a827999; + a = ROL (a, s); + } + + static void HH (ref uint a, uint b, uint c, uint d, uint x, byte s) + { + a += H (b, c, d) + x + 0x6ed9eba1; + a = ROL (a, s); + } + + static void Encode (byte[] output, uint[] input) + { + for (int i = 0, j = 0; j < output.Length; i++, j += 4) { + output[j + 0] = (byte) (input[i]); + output[j + 1] = (byte) (input[i] >> 8); + output[j + 2] = (byte) (input[i] >> 16); + output[j + 3] = (byte) (input[i] >> 24); + } + } + + static void Decode (uint[] output, byte[] input, int index) + { + for (int i = 0, j = index; i < output.Length; i++, j += 4) + output[i] = (uint) ((input[j]) | (input[j + 1] << 8) | (input[j + 2] << 16) | (input[j + 3] << 24)); + } + + void MD4Transform (byte[] block, int index) + { + uint a = state[0]; + uint b = state[1]; + uint c = state[2]; + uint d = state[3]; + + Decode (x, block, index); + + /* Round 1 */ + FF (ref a, b, c, d, x[ 0], S11); /* 1 */ + FF (ref d, a, b, c, x[ 1], S12); /* 2 */ + FF (ref c, d, a, b, x[ 2], S13); /* 3 */ + FF (ref b, c, d, a, x[ 3], S14); /* 4 */ + FF (ref a, b, c, d, x[ 4], S11); /* 5 */ + FF (ref d, a, b, c, x[ 5], S12); /* 6 */ + FF (ref c, d, a, b, x[ 6], S13); /* 7 */ + FF (ref b, c, d, a, x[ 7], S14); /* 8 */ + FF (ref a, b, c, d, x[ 8], S11); /* 9 */ + FF (ref d, a, b, c, x[ 9], S12); /* 10 */ + FF (ref c, d, a, b, x[10], S13); /* 11 */ + FF (ref b, c, d, a, x[11], S14); /* 12 */ + FF (ref a, b, c, d, x[12], S11); /* 13 */ + FF (ref d, a, b, c, x[13], S12); /* 14 */ + FF (ref c, d, a, b, x[14], S13); /* 15 */ + FF (ref b, c, d, a, x[15], S14); /* 16 */ + + /* Round 2 */ + GG (ref a, b, c, d, x[ 0], S21); /* 17 */ + GG (ref d, a, b, c, x[ 4], S22); /* 18 */ + GG (ref c, d, a, b, x[ 8], S23); /* 19 */ + GG (ref b, c, d, a, x[12], S24); /* 20 */ + GG (ref a, b, c, d, x[ 1], S21); /* 21 */ + GG (ref d, a, b, c, x[ 5], S22); /* 22 */ + GG (ref c, d, a, b, x[ 9], S23); /* 23 */ + GG (ref b, c, d, a, x[13], S24); /* 24 */ + GG (ref a, b, c, d, x[ 2], S21); /* 25 */ + GG (ref d, a, b, c, x[ 6], S22); /* 26 */ + GG (ref c, d, a, b, x[10], S23); /* 27 */ + GG (ref b, c, d, a, x[14], S24); /* 28 */ + GG (ref a, b, c, d, x[ 3], S21); /* 29 */ + GG (ref d, a, b, c, x[ 7], S22); /* 30 */ + GG (ref c, d, a, b, x[11], S23); /* 31 */ + GG (ref b, c, d, a, x[15], S24); /* 32 */ + + HH (ref a, b, c, d, x[ 0], S31); /* 33 */ + HH (ref d, a, b, c, x[ 8], S32); /* 34 */ + HH (ref c, d, a, b, x[ 4], S33); /* 35 */ + HH (ref b, c, d, a, x[12], S34); /* 36 */ + HH (ref a, b, c, d, x[ 2], S31); /* 37 */ + HH (ref d, a, b, c, x[10], S32); /* 38 */ + HH (ref c, d, a, b, x[ 6], S33); /* 39 */ + HH (ref b, c, d, a, x[14], S34); /* 40 */ + HH (ref a, b, c, d, x[ 1], S31); /* 41 */ + HH (ref d, a, b, c, x[ 9], S32); /* 42 */ + HH (ref c, d, a, b, x[ 5], S33); /* 43 */ + HH (ref b, c, d, a, x[13], S34); /* 44 */ + HH (ref a, b, c, d, x[ 3], S31); /* 45 */ + HH (ref d, a, b, c, x[11], S32); /* 46 */ + HH (ref c, d, a, b, x[ 7], S33); /* 47 */ + HH (ref b, c, d, a, x[15], S34); /* 48 */ + + state [0] += a; + state [1] += b; + state [2] += c; + state [3] += d; + } + + public byte[] ComputeHash (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || offset > buffer.Length - count) + throw new ArgumentOutOfRangeException (nameof (count)); + + if (disposed) + throw new ObjectDisposedException (nameof (MD4)); + + HashCore (buffer, offset, count); + hashValue = HashFinal (); + Initialize (); + + return hashValue; + } + + public byte[] ComputeHash (byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + return ComputeHash (buffer, 0, buffer.Length); + } + + public byte[] ComputeHash (Stream inputStream) + { + if (inputStream == null) + throw new ArgumentNullException (nameof (inputStream)); + + // don't read stream unless object is ready to use + if (disposed) + throw new ObjectDisposedException (nameof (MD4)); + + var buffer = new byte[4096]; + int nread; + + do { + if ((nread = inputStream.Read (buffer, 0, buffer.Length)) > 0) + HashCore (buffer, 0, nread); + } while (nread > 0); + + hashValue = HashFinal (); + Initialize (); + + return hashValue; + } + + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (inputBuffer == null) + throw new ArgumentNullException (nameof (inputBuffer)); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException (nameof (inputOffset)); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + + if (outputBuffer != null) { + if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException (nameof (outputOffset)); + } + + HashCore (inputBuffer, inputOffset, inputCount); + + if (outputBuffer != null) + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + + return inputCount; + } + + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputCount < 0) + throw new ArgumentOutOfRangeException (nameof (inputCount)); + + var outputBuffer = new byte[inputCount]; + + // note: other exceptions are handled by Buffer.BlockCopy + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, 0, inputCount); + + HashCore (inputBuffer, inputOffset, inputCount); + hashValue = HashFinal (); + Initialize (); + + return outputBuffer; + } + + void Dispose (bool disposing) + { + if (buffered != null) { + Array.Clear (buffered, 0, buffered.Length); + buffered = null; + } + + if (state != null) { + Array.Clear (state, 0, state.Length); + state = null; + } + + if (count != null) { + Array.Clear (count, 0, count.Length); + count = null; + } + + if (x != null) { + Array.Clear (x, 0, x.Length); + x = null; + } + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + disposed = true; + } + } +} diff --git a/src/MailKit/Security/Ntlm/MessageBase.cs b/src/MailKit/Security/Ntlm/MessageBase.cs new file mode 100644 index 0000000..5cc624d --- /dev/null +++ b/src/MailKit/Security/Ntlm/MessageBase.cs @@ -0,0 +1,99 @@ +// +// Mono.Security.Protocol.Ntlm.MessageBase +// abstract class for all NTLM messages +// +// Author: +// Sebastien Pouliot +// +// Copyright (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004 Novell, Inc (http://www.novell.com) +// +// References +// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär +// http://www.innovation.ch/java/ntlm.html +// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass +// http://davenport.sourceforge.net/ntlm.html +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Globalization; + +namespace MailKit.Security.Ntlm { + abstract class MessageBase + { + static readonly byte[] header = { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00 }; + + protected MessageBase (int type) + { + Type = type; + } + + public NtlmFlags Flags { + get; set; + } + + public int Type { + get; private set; + } + + protected byte[] PrepareMessage (int size) + { + var message = new byte[size]; + + Buffer.BlockCopy (header, 0, message, 0, 8); + + message[ 8] = (byte) Type; + message[ 9] = (byte)(Type >> 8); + message[10] = (byte)(Type >> 16); + message[11] = (byte)(Type >> 24); + + return message; + } + + bool CheckHeader (byte[] message, int startIndex) + { + for (int i = 0; i < header.Length; i++) { + if (message[startIndex + i] != header[i]) + return false; + } + + return BitConverterLE.ToUInt32 (message, startIndex + 8) == Type; + } + + protected void ValidateArguments (byte[] message, int startIndex, int length) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (startIndex < 0 || startIndex > message.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 12 || length > (message.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (!CheckHeader (message, startIndex)) + throw new ArgumentException (string.Format (CultureInfo.InvariantCulture, "Invalid Type{0} message.", Type), nameof (message)); + } + + public abstract byte[] Encode (); + } +} diff --git a/src/MailKit/Security/Ntlm/NtlmAuthLevel.cs b/src/MailKit/Security/Ntlm/NtlmAuthLevel.cs new file mode 100644 index 0000000..584d164 --- /dev/null +++ b/src/MailKit/Security/Ntlm/NtlmAuthLevel.cs @@ -0,0 +1,51 @@ +// +// NtlmAuthLevel.cs +// +// Author: +// Martin Baulig +// +// Copyright (c) 2012 Xamarin Inc. (http://www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace MailKit.Security.Ntlm { + /* + * On Windows, this is controlled by a registry setting + * (http://msdn.microsoft.com/en-us/library/ms814176.aspx) + * + * This can be configured by setting the static + * Type3Message.DefaultAuthLevel property, the default value + * is LM_and_NTLM_and_try_NTLMv2_Session. + */ + enum NtlmAuthLevel { + /* Use LM and NTLM, never use NTLMv2 session security. */ + LM_and_NTLM, + + /* Use NTLMv2 session security if the server supports it, + * otherwise fall back to LM and NTLM. */ + LM_and_NTLM_and_try_NTLMv2_Session, + + /* Use NTLMv2 session security if the server supports it, + * otherwise fall back to NTLM. Never use LM. */ + NTLM_only, + + /* Use NTLMv2 only. */ + NTLMv2_only, + } +} diff --git a/src/MailKit/Security/Ntlm/NtlmFlags.cs b/src/MailKit/Security/Ntlm/NtlmFlags.cs new file mode 100644 index 0000000..2d96a33 --- /dev/null +++ b/src/MailKit/Security/Ntlm/NtlmFlags.cs @@ -0,0 +1,222 @@ +// +// NtlmFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit.Security.Ntlm { + /// + /// The NTLM message header flags. + /// + /// + /// More details here: http://davenport.sourceforge.net/ntlm.html#theNtlmMessageHeaderLayout + /// and at https://msdn.microsoft.com/en-us/library/cc236650.aspx + /// + [Flags] + enum NtlmFlags { + /// + /// Indicates that Unicode strings are supported for use in security buffer data. + /// + NegotiateUnicode = 0x00000001, + + /// + /// Indicates that OEM strings are supported for use in security buffer data. + /// + NegotiateOem = 0x00000002, + + /// + /// Requests that the server's authentication realm be included in the Type 2 message. + /// + RequestTarget = 0x00000004, + + /// + /// This flag's usage has not been identified. + /// + R10 = 0x00000008, + + /// + /// Specifies that authenticated communication between the client and server should carry a digital signature (message integrity). + /// + NegotiateSign = 0x00000010, + + /// + /// Specifies that authenticated communication between the client and server should be encrypted (message confidentiality). + /// + NegotiateSeal = 0x00000020, + + /// + /// Indicates that datagram authentication is being used. + /// + NegotiateDatagramStyle = 0x00000040, + + /// + /// Indicates that the Lan Manager Session Key should be used for signing + /// and sealing authenticated communications. + /// + NegotiateLanManagerKey = 0x00000080, + + /// + /// This flag is unused and MUST be zero. (r8) + /// + R9 = 0x00000100, + + /// + /// Indicates that NTLM authentication is being used. + /// + NegotiateNtlm = 0x00000200, + + /// + /// This flag is unused and MUST be zero. (r8) + /// + R8 = 0x00000400, + + /// + /// Sent by the client in the Type 3 message to indicate that an anonymous + /// context has been established. This also affects the response fields. + /// + NegotiateAnonymous = 0x00000800, + + /// + /// Sent by the client in the Type 1 message to indicate that the name of the + /// domain in which the client workstation has membership is included in the + /// message. This is used by the server to determine whether the client is + /// eligible for local authentication. + /// + NegotiateDomainSupplied = 0x00001000, + + /// + /// Sent by the client in the Type 1 message to indicate that the client + /// workstation's name is included in the message. This is used by the server + /// to determine whether the client is eligible for local authentication. + /// + NegotiateWorkstationSupplied = 0x00002000, + + /// + /// Sent by the server to indicate that the server and client are on the same + /// machine. Implies that the client may use the established local credentials + /// for authentication instead of calculating a response to the challenge. + /// + NegotiateLocalCall = 0x00004000, + R7 = NegotiateLocalCall, + + /// + /// Indicates that authenticated communication between the client and server + /// should be signed with a "dummy" signature. + /// + NegotiateAlwaysSign = 0x00008000, + + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a domain. + /// + TargetTypeDomain = 0x00010000, + + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a server. + /// + TargetTypeServer = 0x00020000, + + /// + /// Sent by the server in the Type 2 message to indicate that the target + /// authentication realm is a share. Presumably, this is for share-level + /// authentication. Usage is unclear. + /// + TargetTypeShare = 0x00040000, + R6 = TargetTypeShare, + + /// + /// Indicates that the NTLM2 signing and sealing scheme should be used for + /// protecting authenticated communications. Note that this refers to a + /// particular session security scheme, and is not related to the use of + /// NTLMv2 authentication. This flag can, however, have an effect on the + /// response calculations. + /// + NegotiateNtlm2Key = 0x00080000, + + /// + /// This flag's usage has not been identified. + /// + NegotiateIdentify = 0x00100000, + + /// + /// This flag is unused and MUST be zero. (r5) + /// + R5 = 0x00200000, + + /// + /// Indicates that the LMOWF function should be used to generate a session key. + /// + RequestNonNTSessionKey = 0x00400000, + + /// + /// Sent by the server in the Type 2 message to indicate that it is including + /// a Target Information block in the message. The Target Information block + /// is used in the calculation of the NTLMv2 response. + /// + NegotiateTargetInfo = 0x00800000, + + /// + /// This flag is unused and MUST be zero. (r4) + /// + R4 = 0x01000000, + + /// + /// Indicates that the version field is present. + /// + NegotiateVersion = 0x02000000, + + /// + /// This flag is unused and MUST be zero. (r3) + /// + R3 = 0x04000000, + + /// + /// This flag is unused and MUST be zero. (r2) + /// + R2 = 0x08000000, + + /// + /// This flag is unused and MUST be zero. (r1) + /// + R1 = 0x10000000, + + /// + /// Indicates that 128-bit encryption is supported. + /// + Negotiate128 = 0x20000000, + + /// + /// Indicates that the client will provide an encrypted master key in the + /// "Session Key" field of the Type 3 message. + /// + NegotiateKeyExchange = 0x40000000, + + /// + /// Indicates that 56-bit encryption is supported. + /// + Negotiate56 = (unchecked ((int) 0x80000000)) + } +} diff --git a/src/MailKit/Security/Ntlm/TargetInfo.cs b/src/MailKit/Security/Ntlm/TargetInfo.cs new file mode 100644 index 0000000..946ec77 --- /dev/null +++ b/src/MailKit/Security/Ntlm/TargetInfo.cs @@ -0,0 +1,261 @@ +// +// TargetInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +namespace MailKit.Security.Ntlm { + class TargetInfo + { + public TargetInfo (byte[] buffer, int startIndex, int length, bool unicode) + { + Decode (buffer, startIndex, length, unicode); + } + + public TargetInfo () + { + } + + public int? Flags { + get; set; + } + + public string DomainName { + get; set; + } + + public string ServerName { + get; set; + } + + public string DnsDomainName { + get; set; + } + + public string DnsServerName { + get; set; + } + + public string DnsTreeName { + get; set; + } + + public string TargetName { + get; set; + } + + public long Timestamp { + get; set; + } + + static string DecodeString (byte[] buffer, ref int index, bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + var length = BitConverterLE.ToInt16 (buffer, index); + var value = encoding.GetString (buffer, index + 2, length); + + index += 2 + length; + + return value; + } + + static int DecodeFlags (byte[] buffer, ref int index) + { + short nbytes = BitConverterLE.ToInt16 (buffer, index); + int flags; + + index += 2; + + switch (nbytes) { + case 4: flags = BitConverterLE.ToInt32 (buffer, index); break; + case 2: flags = BitConverterLE.ToInt16 (buffer, index); break; + default: flags = 0; break; + } + + index += nbytes; + + return flags; + } + + static long DecodeTimestamp (byte[] buffer, ref int index) + { + short nbytes = BitConverterLE.ToInt16 (buffer, index); + long lo, hi; + + index += 2; + + switch (nbytes) { + case 8: + lo = BitConverterLE.ToUInt32 (buffer, index); + index += 4; + hi = BitConverterLE.ToUInt32 (buffer, index); + index += 4; + return (hi << 32) | lo; + case 4: + lo = BitConverterLE.ToUInt32 (buffer, index); + index += 4; + return lo; + case 2: + lo = BitConverterLE.ToUInt16 (buffer, index); + index += 2; + return lo; + default: + index += nbytes; + return 0; + } + } + + void Decode (byte[] buffer, int startIndex, int length, bool unicode) + { + int index = startIndex; + + do { + var type = BitConverterLE.ToInt16 (buffer, index); + + index += 2; + + switch (type) { + case 0: index = startIndex + length; break; // a 'type' of 0 terminates the TargetInfo + case 1: ServerName = DecodeString (buffer, ref index, unicode); break; + case 2: DomainName = DecodeString (buffer, ref index, unicode); break; + case 3: DnsServerName = DecodeString (buffer, ref index, unicode); break; + case 4: DnsDomainName = DecodeString (buffer, ref index, unicode); break; + case 5: DnsTreeName = DecodeString (buffer, ref index, unicode); break; + case 6: Flags = DecodeFlags (buffer, ref index); break; + case 7: Timestamp = DecodeTimestamp (buffer, ref index); break; + case 9: TargetName = DecodeString (buffer, ref index, unicode); break; + default: index += 2 + BitConverterLE.ToInt16 (buffer, index); break; + } + } while (index < startIndex + length); + } + + int CalculateSize (bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + int length = 4; + + if (!string.IsNullOrEmpty (DomainName)) + length += 4 + encoding.GetByteCount (DomainName); + + if (!string.IsNullOrEmpty (ServerName)) + length += 4 + encoding.GetByteCount (ServerName); + + if (!string.IsNullOrEmpty (DnsDomainName)) + length += 4 + encoding.GetByteCount (DnsDomainName); + + if (!string.IsNullOrEmpty (DnsServerName)) + length += 4 + encoding.GetByteCount (DnsServerName); + + if (!string.IsNullOrEmpty (DnsTreeName)) + length += 4 + encoding.GetByteCount (DnsTreeName); + + if (Flags.HasValue) + length += 8; + + if (Timestamp != 0) + length += 12; + + if (!string.IsNullOrEmpty (TargetName)) + length += 4 + encoding.GetByteCount (TargetName); + + return length; + } + + static void EncodeTypeAndLength (byte[] buf, ref int index, short type, short length) + { + buf[index++] = (byte) (type); + buf[index++] = (byte) (type >> 8); + buf[index++] = (byte) (length); + buf[index++] = (byte) (length >> 8); + } + + static void EncodeString (byte[] buf, ref int index, short type, string value, bool unicode) + { + var encoding = unicode ? Encoding.Unicode : Encoding.UTF8; + int length = value.Length; + + if (unicode) + length *= 2; + + EncodeTypeAndLength (buf, ref index, type, (short) length); + encoding.GetBytes (value, 0, value.Length, buf, index); + index += length; + } + + static void EncodeInt32 (byte[] buf, ref int index, int value) + { + buf[index++] = (byte) (value); + buf[index++] = (byte) (value >> 8); + buf[index++] = (byte) (value >> 16); + buf[index++] = (byte) (value >> 24); + } + + static void EncodeTimestamp (byte[] buf, ref int index, short type, long value) + { + EncodeTypeAndLength (buf, ref index, type, 8); + EncodeInt32 (buf, ref index, (int) (value & 0xffffffff)); + EncodeInt32 (buf, ref index, (int) (value >> 32)); + } + + static void EncodeFlags (byte[] buf, ref int index, short type, int value) + { + EncodeTypeAndLength (buf, ref index, type, 4); + EncodeInt32 (buf, ref index, value); + } + + public byte[] Encode (bool unicode) + { + var buf = new byte[CalculateSize (unicode)]; + int index = 0; + + if (!string.IsNullOrEmpty (DomainName)) + EncodeString (buf, ref index, 2, DomainName, unicode); + + if (!string.IsNullOrEmpty (ServerName)) + EncodeString (buf, ref index, 1, ServerName, unicode); + + if (!string.IsNullOrEmpty (DnsDomainName)) + EncodeString (buf, ref index, 4, DnsDomainName, unicode); + + if (!string.IsNullOrEmpty (DnsServerName)) + EncodeString (buf, ref index, 3, DnsServerName, unicode); + + if (!string.IsNullOrEmpty (DnsTreeName)) + EncodeString (buf, ref index, 5, DnsTreeName, unicode); + + if (Flags.HasValue) + EncodeFlags (buf, ref index, 6, Flags.Value); + + if (Timestamp != 0) + EncodeTimestamp (buf, ref index, 7, Timestamp); + + if (!string.IsNullOrEmpty (TargetName)) + EncodeString (buf, ref index, 9, TargetName, unicode); + + return buf; + } + } +} diff --git a/src/MailKit/Security/Ntlm/Type1Message.cs b/src/MailKit/Security/Ntlm/Type1Message.cs new file mode 100644 index 0000000..1f15dc7 --- /dev/null +++ b/src/MailKit/Security/Ntlm/Type1Message.cs @@ -0,0 +1,167 @@ +// +// Mono.Security.Protocol.Ntlm.Type1Message - Negotiation +// +// Authors: Sebastien Pouliot +// Jeffrey Stedfast +// +// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (c) 2004 Novell, Inc (http://www.novell.com) +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// References +// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär +// http://www.innovation.ch/java/ntlm.html +// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass +// http://davenport.sourceforge.net/ntlm.html +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Text; + +namespace MailKit.Security.Ntlm { + class Type1Message : MessageBase + { + internal static readonly NtlmFlags DefaultFlags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateOem | NtlmFlags.NegotiateUnicode | NtlmFlags.RequestTarget; + + string domain; + string host; + + public Type1Message (string hostName, string domainName) : base (1) + { + Flags = DefaultFlags; + Domain = domainName; + Host = hostName; + } + + public Type1Message (byte[] message, int startIndex, int length) : base (1) + { + Decode (message, startIndex, length); + } + + public string Domain { + get { return domain; } + set { + if (string.IsNullOrEmpty (value)) { + Flags &= ~NtlmFlags.NegotiateDomainSupplied; + value = string.Empty; + } else { + Flags |= NtlmFlags.NegotiateDomainSupplied; + } + + domain = value; + } + } + + public string Host { + get { return host; } + set { + if (string.IsNullOrEmpty (value)) { + Flags &= ~NtlmFlags.NegotiateWorkstationSupplied; + value = string.Empty; + } else { + Flags |= NtlmFlags.NegotiateWorkstationSupplied; + } + + host = value; + } + } + + public Version OSVersion { + get; set; + } + + void Decode (byte[] message, int startIndex, int length) + { + int offset, count; + + ValidateArguments (message, startIndex, length); + + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 12); + + // decode the domain + count = BitConverterLE.ToUInt16 (message, startIndex + 16); + offset = BitConverterLE.ToUInt16 (message, startIndex + 20); + domain = Encoding.UTF8.GetString (message, startIndex + offset, count); + + // decode the workstation/host + count = BitConverterLE.ToUInt16 (message, startIndex + 24); + offset = BitConverterLE.ToUInt16 (message, startIndex + 28); + host = Encoding.UTF8.GetString (message, startIndex + offset, count); + + if (offset == 40) { + // decode the OS Version + int major = message[startIndex + 32]; + int minor = message[startIndex + 33]; + int build = BitConverterLE.ToUInt16 (message, startIndex + 34); + + OSVersion = new Version (major, minor, build); + } + } + + public override byte[] Encode () + { + int versionLength = OSVersion != null ? 8 : 0; + int hostOffset = 32 + versionLength; + int domainOffset = hostOffset + host.Length; + + var message = PrepareMessage (32 + domain.Length + host.Length + versionLength); + + message[12] = (byte) Flags; + message[13] = (byte)((uint) Flags >> 8); + message[14] = (byte)((uint) Flags >> 16); + message[15] = (byte)((uint) Flags >> 24); + + message[16] = (byte) domain.Length; + message[17] = (byte)(domain.Length >> 8); + message[18] = message[16]; + message[19] = message[17]; + message[20] = (byte) domainOffset; + message[21] = (byte)(domainOffset >> 8); + + message[24] = (byte) host.Length; + message[25] = (byte)(host.Length >> 8); + message[26] = message[24]; + message[27] = message[25]; + message[28] = (byte) hostOffset; + message[29] = (byte)(hostOffset >> 8); + + if (OSVersion != null) { + message[32] = (byte) OSVersion.Major; + message[33] = (byte) OSVersion.Minor; + message[34] = (byte)(OSVersion.Build); + message[35] = (byte)(OSVersion.Build >> 8); + message[36] = 0x00; + message[37] = 0x00; + message[38] = 0x00; + message[39] = 0x0f; + } + + var hostName = Encoding.UTF8.GetBytes (host.ToUpperInvariant ()); + Buffer.BlockCopy (hostName, 0, message, hostOffset, hostName.Length); + + var domainName = Encoding.UTF8.GetBytes (domain.ToUpperInvariant ()); + Buffer.BlockCopy (domainName, 0, message, domainOffset, domainName.Length); + + return message; + } + } +} diff --git a/src/MailKit/Security/Ntlm/Type2Message.cs b/src/MailKit/Security/Ntlm/Type2Message.cs new file mode 100644 index 0000000..d00c254 --- /dev/null +++ b/src/MailKit/Security/Ntlm/Type2Message.cs @@ -0,0 +1,191 @@ +// +// Mono.Security.Protocol.Ntlm.Type2Message - Challenge +// +// Authors: Sebastien Pouliot +// Jeffrey Stedfast +// +// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (c) 2004 Novell, Inc (http://www.novell.com) +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// References +// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär +// http://www.innovation.ch/java/ntlm.html +// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass +// http://davenport.sourceforge.net/ntlm.html +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Text; +using System.Security.Cryptography; + +namespace MailKit.Security.Ntlm { + class Type2Message : MessageBase + { + byte[] targetInfo; + byte[] nonce; + + public Type2Message () : base (2) + { + Flags = NtlmFlags.NegotiateNtlm | NtlmFlags.NegotiateUnicode /*| NtlmFlags.NegotiateAlwaysSign*/; + nonce = new byte[8]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (nonce); + } + + public Type2Message (byte[] message, int startIndex, int length) : base (2) + { + nonce = new byte[8]; + Decode (message, startIndex, length); + } + + ~Type2Message () + { + if (nonce != null) + Array.Clear (nonce, 0, nonce.Length); + } + + public byte[] Nonce { + get { return (byte[]) nonce.Clone (); } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length != 8) + throw new ArgumentException ("Invalid Nonce Length (should be 8 bytes).", nameof (value)); + + nonce = (byte[]) value.Clone (); + } + } + + public string TargetName { + get; set; + } + + public TargetInfo TargetInfo { + get; set; + } + + public byte[] EncodedTargetInfo { + get { + if (targetInfo != null) + return (byte[]) targetInfo.Clone (); + + return new byte[0]; + } + } + + void Decode (byte[] message, int startIndex, int length) + { + ValidateArguments (message, startIndex, length); + + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 20); + + Buffer.BlockCopy (message, startIndex + 24, nonce, 0, 8); + + var targetNameLength = BitConverterLE.ToUInt16 (message, startIndex + 12); + var targetNameOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); + + if (targetNameLength > 0) { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + TargetName = encoding.GetString (message, startIndex + targetNameOffset, targetNameLength); + } + + // The Target Info block is optional. + if (length >= 48 && targetNameOffset >= 48) { + var targetInfoLength = BitConverterLE.ToUInt16 (message, startIndex + 40); + var targetInfoOffset = BitConverterLE.ToUInt16 (message, startIndex + 44); + + if (targetInfoLength > 0 && targetInfoOffset < length && targetInfoLength <= (length - targetInfoOffset)) { + TargetInfo = new TargetInfo (message, startIndex + targetInfoOffset, targetInfoLength, (Flags & NtlmFlags.NegotiateOem) == 0); + + targetInfo = new byte[targetInfoLength]; + Buffer.BlockCopy (message, startIndex + targetInfoOffset, targetInfo, 0, targetInfoLength); + } + } + } + + public override byte[] Encode () + { + int targetNameOffset = 40; + int targetInfoOffset = 48; + byte[] targetName = null; + int size = 40; + + if (TargetName != null) { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + targetName = encoding.GetBytes (TargetName); + targetInfoOffset += targetName.Length; + size += targetName.Length; + } + + if (TargetInfo != null || targetInfo != null) { + if (targetInfo == null) + targetInfo = TargetInfo.Encode ((Flags & NtlmFlags.NegotiateUnicode) != 0); + size += targetInfo.Length + 8; + targetNameOffset += 8; + } + + var data = PrepareMessage (size); + + // message length + short length = (short) data.Length; + data[16] = (byte) length; + data[17] = (byte)(length >> 8); + + // flags + data[20] = (byte) Flags; + data[21] = (byte)((uint) Flags >> 8); + data[22] = (byte)((uint) Flags >> 16); + data[23] = (byte)((uint) Flags >> 24); + + Buffer.BlockCopy (nonce, 0, data, 24, nonce.Length); + + if (targetName != null) { + data[12] = (byte) targetName.Length; + data[13] = (byte)(targetName.Length >> 8); + data[14] = (byte)targetName.Length; + data[15] = (byte)(targetName.Length >> 8); + data[16] = (byte) targetNameOffset; + data[17] = (byte)(targetNameOffset >> 8); + + Buffer.BlockCopy (targetName, 0, data, targetNameOffset, targetName.Length); + } + + if (targetInfo != null) { + data[40] = (byte) targetInfo.Length; + data[41] = (byte)(targetInfo.Length >> 8); + data[42] = (byte) targetInfo.Length; + data[43] = (byte)(targetInfo.Length >> 8); + data[44] = (byte) targetInfoOffset; + data[45] = (byte)(targetInfoOffset >> 8); + + Buffer.BlockCopy (targetInfo, 0, data, targetInfoOffset, targetInfo.Length); + } + + return data; + } + } +} diff --git a/src/MailKit/Security/Ntlm/Type3Message.cs b/src/MailKit/Security/Ntlm/Type3Message.cs new file mode 100644 index 0000000..79e11b9 --- /dev/null +++ b/src/MailKit/Security/Ntlm/Type3Message.cs @@ -0,0 +1,284 @@ +// +// Mono.Security.Protocol.Ntlm.Type3Message - Authentication +// +// Authors: Sebastien Pouliot +// Jeffrey Stedfast +// +// Copyright (c) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (c) 2004 Novell, Inc (http://www.novell.com) +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// References +// a. NTLM Authentication Scheme for HTTP, Ronald Tschalär +// http://www.innovation.ch/java/ntlm.html +// b. The NTLM Authentication Protocol, Copyright © 2003 Eric Glass +// http://davenport.sourceforge.net/ntlm.html +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Text; + +namespace MailKit.Security.Ntlm { + class Type3Message : MessageBase + { + readonly Type2Message type2; + readonly byte[] challenge; + + public Type3Message (byte[] message, int startIndex, int length) : base (3) + { + Decode (message, startIndex, length); + type2 = null; + } + + public Type3Message (Type2Message type2, NtlmAuthLevel level, string userName, string password, string host) : base (3) + { + this.type2 = type2; + + challenge = type2.Nonce; + Domain = type2.TargetName; + Username = userName; + Password = password; + Level = level; + Host = host; + Flags = 0; + + if ((type2.Flags & NtlmFlags.NegotiateUnicode) != 0) + Flags |= NtlmFlags.NegotiateUnicode; + else + Flags |= NtlmFlags.NegotiateOem; + + if ((type2.Flags & NtlmFlags.NegotiateNtlm) != 0) + Flags |= NtlmFlags.NegotiateNtlm; + + if ((type2.Flags & NtlmFlags.NegotiateNtlm2Key) != 0) + Flags |= NtlmFlags.NegotiateNtlm2Key; + + if ((type2.Flags & NtlmFlags.NegotiateVersion) != 0) + Flags |= NtlmFlags.NegotiateVersion; + } + + ~Type3Message () + { + if (challenge != null) + Array.Clear (challenge, 0, challenge.Length); + + if (LM != null) + Array.Clear (LM, 0, LM.Length); + + if (NT != null) + Array.Clear (NT, 0, NT.Length); + } + + public NtlmAuthLevel Level { + get; set; + } + + public string Domain { + get; set; + } + + public string Host { + get; set; + } + + public string Password { + get; set; + } + + public string Username { + get; set; + } + + public byte[] LM { + get; private set; + } + + public byte[] NT { + get; set; + } + + void Decode (byte[] message, int startIndex, int length) + { + ValidateArguments (message, startIndex, length); + + Password = null; + + if (message.Length >= 64) + Flags = (NtlmFlags) BitConverterLE.ToUInt32 (message, startIndex + 60); + else + Flags = (NtlmFlags) 0x8201; + + int lmLength = BitConverterLE.ToUInt16 (message, startIndex + 12); + int lmOffset = BitConverterLE.ToUInt16 (message, startIndex + 16); + LM = new byte[lmLength]; + Buffer.BlockCopy (message, startIndex + lmOffset, LM, 0, lmLength); + + int ntLength = BitConverterLE.ToUInt16 (message, startIndex + 20); + int ntOffset = BitConverterLE.ToUInt16 (message, startIndex + 24); + NT = new byte[ntLength]; + Buffer.BlockCopy (message, startIndex + ntOffset, NT, 0, ntLength); + + int domainLength = BitConverterLE.ToUInt16 (message, startIndex + 28); + int domainOffset = BitConverterLE.ToUInt16 (message, startIndex + 32); + Domain = DecodeString (message, startIndex + domainOffset, domainLength); + + int userLength = BitConverterLE.ToUInt16 (message, startIndex + 36); + int userOffset = BitConverterLE.ToUInt16 (message, startIndex + 40); + Username = DecodeString (message, startIndex + userOffset, userLength); + + int hostLength = BitConverterLE.ToUInt16 (message, startIndex + 44); + int hostOffset = BitConverterLE.ToUInt16 (message, startIndex + 48); + Host = DecodeString (message, startIndex + hostOffset, hostLength); + + // Session key. We don't use it yet. + // int skeyLength = BitConverterLE.ToUInt16 (message, startIndex + 52); + // int skeyOffset = BitConverterLE.ToUInt16 (message, startIndex + 56); + } + + string DecodeString (byte[] buffer, int offset, int len) + { + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + return encoding.GetString (buffer, offset, len); + } + + byte[] EncodeString (string text) + { + if (text == null) + return new byte[0]; + + var encoding = (Flags & NtlmFlags.NegotiateUnicode) != 0 ? Encoding.Unicode : Encoding.UTF8; + + return encoding.GetBytes (text); + } + + public override byte[] Encode () + { + var target = EncodeString (Domain); + var user = EncodeString (Username); + var host = EncodeString (Host); + var payloadOffset = 64; + bool reqVersion; + byte[] lm, ntlm; + + ChallengeResponse2.Compute (type2, Level, Username, Password, Domain, out lm, out ntlm); + + if (reqVersion = (type2.Flags & NtlmFlags.NegotiateVersion) != 0) + payloadOffset += 8; + + var lmResponseLength = lm != null ? lm.Length : 0; + var ntResponseLength = ntlm != null ? ntlm.Length : 0; + + var data = PrepareMessage (payloadOffset + target.Length + user.Length + host.Length + lmResponseLength + ntResponseLength); + + // LM response + short lmResponseOffset = (short) (payloadOffset + target.Length + user.Length + host.Length); + data[12] = (byte) lmResponseLength; + data[13] = (byte) 0x00; + data[14] = data[12]; + data[15] = data[13]; + data[16] = (byte) lmResponseOffset; + data[17] = (byte) (lmResponseOffset >> 8); + + // NT response + short ntResponseOffset = (short) (lmResponseOffset + lmResponseLength); + data[20] = (byte) ntResponseLength; + data[21] = (byte) (ntResponseLength >> 8); + data[22] = data[20]; + data[23] = data[21]; + data[24] = (byte) ntResponseOffset; + data[25] = (byte) (ntResponseOffset >> 8); + + // target + short domainLength = (short) target.Length; + short domainOffset = (short) payloadOffset; + data[28] = (byte) domainLength; + data[29] = (byte) (domainLength >> 8); + data[30] = data[28]; + data[31] = data[29]; + data[32] = (byte) domainOffset; + data[33] = (byte) (domainOffset >> 8); + + // username + short userLength = (short) user.Length; + short userOffset = (short) (domainOffset + domainLength); + data[36] = (byte) userLength; + data[37] = (byte) (userLength >> 8); + data[38] = data[36]; + data[39] = data[37]; + data[40] = (byte) userOffset; + data[41] = (byte) (userOffset >> 8); + + // host + short hostLength = (short) host.Length; + short hostOffset = (short) (userOffset + userLength); + data[44] = (byte) hostLength; + data[45] = (byte) (hostLength >> 8); + data[46] = data[44]; + data[47] = data[45]; + data[48] = (byte) hostOffset; + data[49] = (byte) (hostOffset >> 8); + + // message length + short messageLength = (short) data.Length; + data[56] = (byte) messageLength; + data[57] = (byte) (messageLength >> 8); + + // options flags + data[60] = (byte) Flags; + data[61] = (byte)((uint) Flags >> 8); + data[62] = (byte)((uint) Flags >> 16); + data[63] = (byte)((uint) Flags >> 24); + + if (reqVersion) { + // encode the Windows version as Windows 10.0 + data[64] = 0x0A; + data[65] = 0x0; + + // encode the ProductBuild version + data[66] = (byte) (10586 & 0xff); + data[67] = (byte) (10586 >> 8); + + // next 3 bytes are reserved and should remain 0 + + // encode the NTLMRevisionCurrent version + data[71] = 0x0F; + } + + Buffer.BlockCopy (target, 0, data, domainOffset, target.Length); + Buffer.BlockCopy (user, 0, data, userOffset, user.Length); + Buffer.BlockCopy (host, 0, data, hostOffset, host.Length); + + if (lm != null) { + Buffer.BlockCopy (lm, 0, data, lmResponseOffset, lm.Length); + Array.Clear (lm, 0, lm.Length); + } + + if (ntlm != null) { + Buffer.BlockCopy (ntlm, 0, data, ntResponseOffset, ntlm.Length); + Array.Clear (ntlm, 0, ntlm.Length); + } + + return data; + } + } +} diff --git a/src/MailKit/Security/RandomNumberGenerator.cs b/src/MailKit/Security/RandomNumberGenerator.cs new file mode 100644 index 0000000..eee2d5c --- /dev/null +++ b/src/MailKit/Security/RandomNumberGenerator.cs @@ -0,0 +1,52 @@ +// +// RandomNumberGenerator.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using Windows.Security.Cryptography; + +namespace MailKit.Security { + class RandomNumberGenerator : IDisposable + { + public static RandomNumberGenerator Create () + { + return new RandomNumberGenerator (); + } + + public void GetBytes (byte[] bytes) + { + var buffer = CryptographicBuffer.GenerateRandom ((uint) bytes.Length); + byte[] rand; + + CryptographicBuffer.CopyToByteArray (buffer, out rand); + + Array.Copy (rand, 0, bytes, 0, rand.Length); + } + + public void Dispose () + { + } + } +} diff --git a/src/MailKit/Security/SaslException.cs b/src/MailKit/Security/SaslException.cs new file mode 100644 index 0000000..b3b78b1 --- /dev/null +++ b/src/MailKit/Security/SaslException.cs @@ -0,0 +1,158 @@ +// +// SaslException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Security; +using System.Runtime.Serialization; + +namespace MailKit.Security { + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + /// + /// An enumeration of the possible error codes that may be reported by a . + /// + public enum SaslErrorCode { + /// + /// The server's challenge was too long. + /// + ChallengeTooLong, + + /// + /// The server's response contained an incomplete challenge. + /// + IncompleteChallenge, + + /// + /// The server's challenge was invalid. + /// + InvalidChallenge, + + /// + /// The server's response did not contain a challenge. + /// + MissingChallenge, + + /// + /// The server's challenge contained an incorrect hash. + /// + IncorrectHash + } + + /// + /// A SASL authentication exception. + /// + /// + /// Typically indicates an error while parsing a server's challenge token. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class SaslException : AuthenticationException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + protected SaslException (SerializationInfo info, StreamingContext context) : base (info, context) + { + ErrorCode = (SaslErrorCode) info.GetInt32 ("ErrorCode"); + Mechanism = info.GetString ("Mechanism"); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The SASL mechanism. + /// The error code. + /// The error message. + /// + /// is null. + /// + public SaslException (string mechanism, SaslErrorCode code, string message) : base (message) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + Mechanism = mechanism; + ErrorCode = code; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Serializes the state of the . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("ErrorCode", (int) ErrorCode); + info.AddValue ("Mechanism", Mechanism); + } +#endif + + /// + /// Gets the error code. + /// + /// + /// Gets the error code. + /// + /// The error code. + public SaslErrorCode ErrorCode { + get; private set; + } + + /// + /// Gets the name of the SASL mechanism that had the error. + /// + /// + /// Gets the name of the SASL mechanism that had the error. + /// + /// The name of the SASL mechanism. + public string Mechanism { + get; private set; + } + } +} diff --git a/src/MailKit/Security/SaslMechanism.cs b/src/MailKit/Security/SaslMechanism.cs new file mode 100644 index 0000000..0eb9c77 --- /dev/null +++ b/src/MailKit/Security/SaslMechanism.cs @@ -0,0 +1,602 @@ +// +// SaslMechanism.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Security.Cryptography; + +#if NETSTANDARD1_3 || NETSTANDARD1_6 +using MD5 = MimeKit.Cryptography.MD5; +#endif + +namespace MailKit.Security { + /// + /// A SASL authentication mechanism. + /// + /// + /// Authenticating via a SASL mechanism may be a multi-step process. + /// To determine if the mechanism has completed the necessary steps + /// to authentication, check the after + /// each call to . + /// + public abstract class SaslMechanism + { + /// + /// The supported authentication mechanisms in order of strongest to weakest. + /// + /// + /// Used by the various clients when authenticating via SASL to determine + /// which order the SASL mechanisms supported by the server should be tried. + /// + public static readonly string[] AuthMechanismRank = { + "SCRAM-SHA-256", "SCRAM-SHA-1", "CRAM-MD5", "DIGEST-MD5", "PLAIN", "LOGIN" + }; + static readonly bool md5supported; + + static SaslMechanism () + { + try { + using (var md5 = MD5.Create ()) + md5supported = true; + } catch { + md5supported = false; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanism(NetworkCredential) instead.")] + protected SaslMechanism (Uri uri, ICredentials credentials) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + Credentials = credentials.GetCredential (uri, MechanismName); + Uri = uri; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanism(string, string) instead.")] + protected SaslMechanism (Uri uri, string userName, string password) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + Credentials = new NetworkCredential (userName, password); + Uri = uri; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + protected SaslMechanism (NetworkCredential credentials) + { + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + Credentials = credentials; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + protected SaslMechanism (string userName, string password) + { + if (userName == null) + throw new ArgumentNullException (nameof (userName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + Credentials = new NetworkCredential (userName, password); + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public abstract string MechanismName { + get; + } + + /// + /// Gets the user's credentials. + /// + /// + /// Gets the user's credentials. + /// + /// The user's credentials. + public NetworkCredential Credentials { + get; private set; + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public virtual bool SupportsInitialResponse { + get { return false; } + } + + /// + /// Gets or sets whether the SASL mechanism has finished authenticating. + /// + /// + /// Gets or sets whether the SASL mechanism has finished authenticating. + /// + /// true if the SASL mechanism has finished authenticating; otherwise, false. + public bool IsAuthenticated { + get; protected set; + } + + /// + /// Gets whether or not a security layer was negotiated. + /// + /// + /// Gets whether or not a security layer has been negotiated by the SASL mechanism. + /// Some SASL mechanisms, such as GSSAPI, are able to negotiate security layers + /// such as integrity and confidentiality protection. + /// + /// true if a security layer was negotiated; otherwise, false. + public virtual bool NegotiatedSecurityLayer { + get { return false; } + } + + /// + /// Gets or sets the URI of the service. + /// + /// + /// Gets or sets the URI of the service. + /// + /// The URI of the service. + internal Uri Uri { + get; set; + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// THe SASL mechanism does not support SASL-IR. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected abstract byte[] Challenge (byte[] token, int startIndex, int length); + + /// + /// Decodes the base64-encoded server challenge and returns the next challenge response encoded in base64. + /// + /// + /// Decodes the base64-encoded server challenge and returns the next challenge response encoded in base64. + /// + /// The next base64-encoded challenge response. + /// The server's base64-encoded challenge token. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// THe SASL mechanism does not support SASL-IR. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + public string Challenge (string token) + { + byte[] decoded = null; + int length = 0; + + if (token != null) { + try { + decoded = Convert.FromBase64String (token.Trim ()); + length = decoded.Length; + } catch (FormatException) { + } + } + + var challenge = Challenge (decoded, 0, length); + + if (challenge == null || challenge.Length == 0) + return string.Empty; + + return Convert.ToBase64String (challenge); + } + + /// + /// Resets the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public virtual void Reset () + { + IsAuthenticated = false; + } + + /// + /// Determines if the specified SASL mechanism is supported by MailKit. + /// + /// + /// Use this method to make sure that a SASL mechanism is supported before calling + /// . + /// + /// true if the specified SASL mechanism is supported; otherwise, false. + /// The name of the SASL mechanism. + /// + /// is null. + /// + public static bool IsSupported (string mechanism) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + switch (mechanism) { + case "SCRAM-SHA-256": return true; + case "SCRAM-SHA-1": return true; + case "DIGEST-MD5": return md5supported; + case "CRAM-MD5": return md5supported; + case "XOAUTH2": return true; + case "PLAIN": return true; + case "LOGIN": return true; + case "NTLM": return true; + default: return false; + } + } + + /// + /// Create an instance of the specified SASL mechanism using the uri and credentials. + /// + /// + /// If unsure that a particular SASL mechanism is supported, you should first call + /// . + /// + /// An instance of the requested SASL mechanism if supported; otherwise null. + /// The name of the SASL mechanism. + /// The URI of the service to authenticate against. + /// The text encoding to use for the credentials. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public static SaslMechanism Create (string mechanism, Uri uri, Encoding encoding, ICredentials credentials) + { + if (mechanism == null) + throw new ArgumentNullException (nameof (mechanism)); + + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (credentials == null) + throw new ArgumentNullException (nameof (credentials)); + + var cred = credentials.GetCredential (uri, mechanism); + + switch (mechanism) { + //case "KERBEROS_V4": return null; + case "SCRAM-SHA-256": return new SaslMechanismScramSha256 (cred) { Uri = uri }; + case "SCRAM-SHA-1": return new SaslMechanismScramSha1 (cred) { Uri = uri }; + case "DIGEST-MD5": return md5supported ? new SaslMechanismDigestMd5 (cred) { Uri = uri } : null; + case "CRAM-MD5": return md5supported ? new SaslMechanismCramMd5 (cred) { Uri = uri } : null; + //case "GSSAPI": return null; + case "XOAUTH2": return new SaslMechanismOAuth2 (cred) { Uri = uri }; + case "PLAIN": return new SaslMechanismPlain (encoding, cred) { Uri = uri }; + case "LOGIN": return new SaslMechanismLogin (encoding, cred) { Uri = uri }; + case "NTLM": return new SaslMechanismNtlm (cred) { Uri = uri }; + default: return null; + } + } + + /// + /// Create an instance of the specified SASL mechanism using the uri and credentials. + /// + /// + /// If unsure that a particular SASL mechanism is supported, you should first call + /// . + /// + /// An instance of the requested SASL mechanism if supported; otherwise null. + /// The name of the SASL mechanism. + /// The URI of the service to authenticate against. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public static SaslMechanism Create (string mechanism, Uri uri, ICredentials credentials) + { + return Create (mechanism, uri, Encoding.UTF8, credentials); + } + + /// + /// Determines if the character is a non-ASCII space. + /// + /// + /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-C.1.2 + /// + /// true if the character is a non-ASCII space; otherwise, false. + /// The character. + static bool IsNonAsciiSpace (char c) + { + switch (c) { + case '\u00A0': // NO-BREAK SPACE + case '\u1680': // OGHAM SPACE MARK + case '\u2000': // EN QUAD + case '\u2001': // EM QUAD + case '\u2002': // EN SPACE + case '\u2003': // EM SPACE + case '\u2004': // THREE-PER-EM SPACE + case '\u2005': // FOUR-PER-EM SPACE + case '\u2006': // SIX-PER-EM SPACE + case '\u2007': // FIGURE SPACE + case '\u2008': // PUNCTUATION SPACE + case '\u2009': // THIN SPACE + case '\u200A': // HAIR SPACE + case '\u200B': // ZERO WIDTH SPACE + case '\u202F': // NARROW NO-BREAK SPACE + case '\u205F': // MEDIUM MATHEMATICAL SPACE + case '\u3000': // IDEOGRAPHIC SPACE + return true; + default: + return false; + } + } + + /// + /// Determines if the character is commonly mapped to nothing. + /// + /// + /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-B.1 + /// + /// true if the character is commonly mapped to nothing; otherwise, false. + /// The character. + static bool IsCommonlyMappedToNothing (char c) + { + switch (c) { + case '\u00AD': case '\u034F': case '\u1806': + case '\u180B': case '\u180C': case '\u180D': + case '\u200B': case '\u200C': case '\u200D': + case '\u2060': case '\uFE00': case '\uFE01': + case '\uFE02': case '\uFE03': case '\uFE04': + case '\uFE05': case '\uFE06': case '\uFE07': + case '\uFE08': case '\uFE09': case '\uFE0A': + case '\uFE0B': case '\uFE0C': case '\uFE0D': + case '\uFE0E': case '\uFE0F': case '\uFEFF': + return true; + default: + return false; + } + } + + /// + /// Determines if the character is prohibited. + /// + /// + /// This list was obtained from http://tools.ietf.org/html/rfc3454#appendix-C.3 + /// + /// true if the character is prohibited; otherwise, false. + /// The string. + /// The character index. + static bool IsProhibited (string s, int index) + { + int u = char.ConvertToUtf32 (s, index); + + // Private Use characters: http://tools.ietf.org/html/rfc3454#appendix-C.3 + if ((u >= 0xE000 && u <= 0xF8FF) || (u >= 0xF0000 && u <= 0xFFFFD) || (u >= 0x100000 && u <= 0x10FFFD)) + return true; + + // Non-character code points: http://tools.ietf.org/html/rfc3454#appendix-C.4 + if ((u >= 0xFDD0 && u <= 0xFDEF) || (u >= 0xFFFE && u <= 0xFFFF) || (u >= 0x1FFFE && u <= 0x1FFFF) || + (u >= 0x2FFFE && u <= 0x2FFFF) || (u >= 0x3FFFE && u <= 0x3FFFF) || (u >= 0x4FFFE && u <= 0x4FFFF) || + (u >= 0x5FFFE && u <= 0x5FFFF) || (u >= 0x6FFFE && u <= 0x6FFFF) || (u >= 0x7FFFE && u <= 0x7FFFF) || + (u >= 0x8FFFE && u <= 0x8FFFF) || (u >= 0x9FFFE && u <= 0x9FFFF) || (u >= 0xAFFFE && u <= 0xAFFFF) || + (u >= 0xBFFFE && u <= 0xBFFFF) || (u >= 0xCFFFE && u <= 0xCFFFF) || (u >= 0xDFFFE && u <= 0xDFFFF) || + (u >= 0xEFFFE && u <= 0xEFFFF) || (u >= 0xFFFFE && u <= 0xFFFFF) || (u >= 0x10FFFE && u <= 0x10FFFF)) + return true; + + // Surrogate code points: http://tools.ietf.org/html/rfc3454#appendix-C.5 + if (u >= 0xD800 && u <= 0xDFFF) + return true; + + // Inappropriate for plain text characters: http://tools.ietf.org/html/rfc3454#appendix-C.6 + switch (u) { + case 0xFFF9: // INTERLINEAR ANNOTATION ANCHOR + case 0xFFFA: // INTERLINEAR ANNOTATION SEPARATOR + case 0xFFFB: // INTERLINEAR ANNOTATION TERMINATOR + case 0xFFFC: // OBJECT REPLACEMENT CHARACTER + case 0xFFFD: // REPLACEMENT CHARACTER + return true; + } + + // Inappropriate for canonical representation: http://tools.ietf.org/html/rfc3454#appendix-C.7 + if (u >= 0x2FF0 && u <= 0x2FFB) + return true; + + // Change display properties or are deprecated: http://tools.ietf.org/html/rfc3454#appendix-C.8 + switch (u) { + case 0x0340: // COMBINING GRAVE TONE MARK + case 0x0341: // COMBINING ACUTE TONE MARK + case 0x200E: // LEFT-TO-RIGHT MARK + case 0x200F: // RIGHT-TO-LEFT MARK + case 0x202A: // LEFT-TO-RIGHT EMBEDDING + case 0x202B: // RIGHT-TO-LEFT EMBEDDING + case 0x202C: // POP DIRECTIONAL FORMATTING + case 0x202D: // LEFT-TO-RIGHT OVERRIDE + case 0x202E: // RIGHT-TO-LEFT OVERRIDE + case 0x206A: // INHIBIT SYMMETRIC SWAPPING + case 0x206B: // ACTIVATE SYMMETRIC SWAPPING + case 0x206C: // INHIBIT ARABIC FORM SHAPING + case 0x206D: // ACTIVATE ARABIC FORM SHAPING + case 0x206E: // NATIONAL DIGIT SHAPES + case 0x206F: // NOMINAL DIGIT SHAPES + return true; + } + + // Tagging characters: http://tools.ietf.org/html/rfc3454#appendix-C.9 + if (u == 0xE0001 || (u >= 0xE0020 && u <= 0xE007F)) + return true; + + return false; + } + + /// + /// Prepares the user name or password string. + /// + /// + /// Prepares a user name or password string according to the rules of rfc4013. + /// + /// The prepared string. + /// The string to prepare. + /// + /// is null. + /// + /// + /// contains prohibited characters. + /// + public static string SaslPrep (string s) + { + if (s == null) + throw new ArgumentNullException (nameof (s)); + + if (s.Length == 0) + return s; + + var builder = new StringBuilder (s.Length); + for (int i = 0; i < s.Length; i++) { + if (IsNonAsciiSpace (s[i])) { + // non-ASII space characters [StringPrep, C.1.2] that can be + // mapped to SPACE (U+0020). + builder.Append (' '); + } else if (IsCommonlyMappedToNothing (s[i])) { + // the "commonly mapped to nothing" characters [StringPrep, B.1] + // that can be mapped to nothing. + } else if (char.IsControl (s[i])) { + throw new ArgumentException ("Control characters are prohibited.", nameof (s)); + } else if (IsProhibited (s, i)) { + throw new ArgumentException ("One or more characters in the string are prohibited.", nameof (s)); + } else { + builder.Append (s[i]); + } + } + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + return builder.ToString ().Normalize (NormalizationForm.FormKC); +#else + return builder.ToString (); +#endif + } + + internal static string GenerateEntropy (int n) + { + var entropy = new byte[n]; + + using (var rng = RandomNumberGenerator.Create ()) + rng.GetBytes (entropy); + + return Convert.ToBase64String (entropy); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismCramMd5.cs b/src/MailKit/Security/SaslMechanismCramMd5.cs new file mode 100644 index 0000000..9f37e45 --- /dev/null +++ b/src/MailKit/Security/SaslMechanismCramMd5.cs @@ -0,0 +1,215 @@ +// +// SaslMechanismCramMd5.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; + +#if NETSTANDARD1_3 || NETSTANDARD1_6 +using MD5 = MimeKit.Cryptography.MD5; +#else +using System.Security.Cryptography; +#endif + +namespace MailKit.Security { + /// + /// The CRAM-MD5 SASL mechanism. + /// + /// + /// A SASL mechanism based on HMAC-MD5. + /// + public class SaslMechanismCramMd5 : SaslMechanism + { + static readonly byte[] HexAlphabet = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7' + 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, // '8' -> 'f' + }; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new CRAM-MD5 SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismCramMd5(NetworkCredential) instead.")] + public SaslMechanismCramMd5 (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new CRAM-MD5 SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismCramMd5(string, string) instead.")] + public SaslMechanismCramMd5 (Uri uri, string userName, string password) : base (uri, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new CRAM-MD5 SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismCramMd5 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new CRAM-MD5 SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismCramMd5 (string userName, string password) : base (userName, password) + { + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "CRAM-MD5"; } + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + if (token == null) + throw new NotSupportedException ("CRAM-MD5 does not support SASL-IR."); + + var userName = Encoding.UTF8.GetBytes (Credentials.UserName); + var password = Encoding.UTF8.GetBytes (Credentials.Password); + var ipad = new byte[64]; + var opad = new byte[64]; + byte[] digest; + + if (password.Length > 64) { + byte[] checksum; + + using (var md5 = MD5.Create ()) + checksum = md5.ComputeHash (password); + + Array.Copy (checksum, ipad, checksum.Length); + Array.Copy (checksum, opad, checksum.Length); + } else { + Array.Copy (password, ipad, password.Length); + Array.Copy (password, opad, password.Length); + } + + Array.Clear (password, 0, password.Length); + + for (int i = 0; i < 64; i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + using (var md5 = MD5.Create ()) { + md5.TransformBlock (ipad, 0, ipad.Length, null, 0); + md5.TransformFinalBlock (token, startIndex, length); + digest = md5.Hash; + } + + using (var md5 = MD5.Create ()) { + md5.TransformBlock (opad, 0, opad.Length, null, 0); + md5.TransformFinalBlock (digest, 0, digest.Length); + digest = md5.Hash; + } + + var buffer = new byte[userName.Length + 1 + (digest.Length * 2)]; + int offset = 0; + + for (int i = 0; i < userName.Length; i++) + buffer[offset++] = userName[i]; + buffer[offset++] = 0x20; + for (int i = 0; i < digest.Length; i++) { + byte c = digest[i]; + + buffer[offset++] = HexAlphabet[(c >> 4) & 0x0f]; + buffer[offset++] = HexAlphabet[c & 0x0f]; + } + + IsAuthenticated = true; + + return buffer; + } + } +} diff --git a/src/MailKit/Security/SaslMechanismDigestMd5.cs b/src/MailKit/Security/SaslMechanismDigestMd5.cs new file mode 100644 index 0000000..8827cbb --- /dev/null +++ b/src/MailKit/Security/SaslMechanismDigestMd5.cs @@ -0,0 +1,571 @@ +// +// SaslMechanismDigestMd5.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Globalization; +using System.Collections.Generic; +using System.Security.Cryptography; + +#if NETSTANDARD1_3 || NETSTANDARD1_6 +using MD5 = MimeKit.Cryptography.MD5; +#endif + +namespace MailKit.Security { + /// + /// The DIGEST-MD5 SASL mechanism. + /// + /// + /// Unlike the PLAIN and LOGIN SASL mechanisms, the DIGEST-MD5 mechanism + /// provides some level of protection and should be relatively safe to + /// use even with a clear-text connection. + /// + public class SaslMechanismDigestMd5 : SaslMechanism + { + static readonly Encoding Latin1; + + enum LoginState { + Auth, + Final + } + + DigestChallenge challenge; + DigestResponse response; + internal string cnonce; + Encoding encoding; + LoginState state; + + static SaslMechanismDigestMd5 () + { + try { + Latin1 = Encoding.GetEncoding (28591); + } catch (NotSupportedException) { + Latin1 = Encoding.GetEncoding (1252); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new DIGEST-MD5 SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismDigestMd5(NetworkCredential) instead.")] + public SaslMechanismDigestMd5 (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new DIGEST-MD5 SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismDigestMd5(string, string) instead.")] + public SaslMechanismDigestMd5 (Uri uri, string userName, string password) : base (uri, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new DIGEST-MD5 SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismDigestMd5 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new DIGEST-MD5 SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismDigestMd5 (string userName, string password) : base (userName, password) + { + } + + /// + /// Gets or sets the authorization identifier. + /// + /// + /// The authorization identifier is the desired user account that the server should use + /// for all accesses. This is separate from the user name used for authentication. + /// + /// The authorization identifier. + public string AuthorizationId { + get; set; + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "DIGEST-MD5"; } + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// THe SASL mechanism does not support SASL-IR. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + if (token == null) + throw new NotSupportedException ("DIGEST-MD5 does not support SASL-IR."); + + switch (state) { + case LoginState.Auth: + if (token.Length > 2048) + throw new SaslException (MechanismName, SaslErrorCode.ChallengeTooLong, "Server challenge too long."); + + challenge = DigestChallenge.Parse (Encoding.UTF8.GetString (token, startIndex, length)); + encoding = challenge.Charset != null ? Encoding.UTF8 : Latin1; + cnonce = cnonce ?? GenerateEntropy (15); + + response = new DigestResponse (challenge, encoding, Uri.Scheme, Uri.DnsSafeHost, AuthorizationId, Credentials.UserName, Credentials.Password, cnonce); + state = LoginState.Final; + + return response.Encode (encoding); + case LoginState.Final: + if (token.Length == 0) + throw new SaslException (MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); + + var text = encoding.GetString (token, startIndex, length); + string key, value; + + if (!DigestChallenge.TryParseKeyValuePair (text, out key, out value)) + throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Server response contained incomplete authentication data."); + + if (!key.Equals ("rspauth", StringComparison.OrdinalIgnoreCase)) + throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Server response contained invalid data."); + + var expected = response.ComputeHash (encoding, Credentials.Password, false); + if (value != expected) + throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Server response did not contain the expected hash."); + + IsAuthenticated = true; + break; + } + + return null; + } + + /// + /// Resets the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public override void Reset () + { + state = LoginState.Auth; + challenge = null; + response = null; + cnonce = null; + base.Reset (); + } + } + + class DigestChallenge + { + public string[] Realms { get; private set; } + public string Nonce { get; private set; } + public HashSet Qop { get; private set; } + public bool? Stale { get; private set; } + public int? MaxBuf { get; private set; } + public string Charset { get; private set; } + public string Algorithm { get; private set; } + public HashSet Ciphers { get; private set; } + + DigestChallenge () + { + Ciphers = new HashSet (StringComparer.Ordinal); + Qop = new HashSet (StringComparer.Ordinal); + } + + static bool SkipWhiteSpace (string text, ref int index) + { + int startIndex = index; + + while (index < text.Length && char.IsWhiteSpace (text[index])) + index++; + + return index > startIndex; + } + + static string GetKey (string text, ref int index) + { + int startIndex = index; + + while (index < text.Length && !char.IsWhiteSpace (text[index]) && text[index] != '=' && text[index] != ',') + index++; + + return text.Substring (startIndex, index - startIndex); + } + + static bool TryParseQuoted (string text, ref int index, out string value) + { + var builder = new StringBuilder (); + bool escaped = false; + + value = null; + + // skip over leading '"' + index++; + + while (index < text.Length) { + if (text[index] == '\\') { + if (escaped) + builder.Append (text[index]); + + escaped = !escaped; + } else if (!escaped) { + if (text[index] == '"') + break; + + builder.Append (text[index]); + } else { + escaped = false; + } + + index++; + } + + if (index >= text.Length || text[index] != '"') + return false; + + index++; + + value = builder.ToString (); + + return true; + } + + static bool TryParseValue (string text, ref int index, out string value) + { + if (text[index] == '"') + return TryParseQuoted (text, ref index, out value); + + int startIndex = index; + + value = null; + + while (index < text.Length && !char.IsWhiteSpace (text[index]) && text[index] != ',') + index++; + + value = text.Substring (startIndex, index - startIndex); + + return true; + } + + static bool TryParseKeyValuePair (string text, ref int index, out string key, out string value) + { + value = null; + + key = GetKey (text, ref index); + + SkipWhiteSpace (text, ref index); + if (index >= text.Length || text[index] != '=') + return false; + + // skip over '=' + index++; + + SkipWhiteSpace (text, ref index); + if (index >= text.Length) + return false; + + return TryParseValue (text, ref index, out value); + } + + public static bool TryParseKeyValuePair (string text, out string key, out string value) + { + int index = 0; + + value = null; + key = null; + + SkipWhiteSpace (text, ref index); + if (index >= text.Length || !TryParseKeyValuePair (text, ref index, out key, out value)) + return false; + + return true; + } + + public static DigestChallenge Parse (string token) + { + var challenge = new DigestChallenge (); + int index = 0; + int maxbuf; + + SkipWhiteSpace (token, ref index); + + while (index < token.Length) { + string key, value; + + if (!TryParseKeyValuePair (token, ref index, out key, out value)) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + + switch (key.ToLowerInvariant ()) { + case "realm": + challenge.Realms = value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries); + break; + case "nonce": + if (challenge.Nonce != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + challenge.Nonce = value; + break; + case "qop": + foreach (var qop in value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + challenge.Qop.Add (qop.Trim ()); + break; + case "stale": + if (challenge.Stale.HasValue) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + challenge.Stale = value.ToLowerInvariant () == "true"; + break; + case "maxbuf": + if (challenge.MaxBuf.HasValue || !int.TryParse (value, NumberStyles.None, CultureInfo.InvariantCulture, out maxbuf)) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + challenge.MaxBuf = maxbuf; + break; + case "charset": + if (challenge.Charset != null || !value.Equals ("utf-8", StringComparison.OrdinalIgnoreCase)) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + challenge.Charset = "utf-8"; + break; + case "algorithm": + if (challenge.Algorithm != null) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + challenge.Algorithm = value; + break; + case "cipher": + if (challenge.Ciphers.Count > 0) + throw new SaslException ("DIGEST-MD5", SaslErrorCode.InvalidChallenge, string.Format ("Invalid SASL challenge from the server: {0}", token)); + foreach (var cipher in value.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + challenge.Ciphers.Add (cipher.Trim ()); + break; + } + + SkipWhiteSpace (token, ref index); + if (index < token.Length && token[index] == ',') { + index++; + + SkipWhiteSpace (token, ref index); + } + } + + return challenge; + } + } + + class DigestResponse + { + public string UserName { get; private set; } + public string Realm { get; private set; } + public string Nonce { get; private set; } + public string CNonce { get; private set; } + public int Nc { get; private set; } + public string Qop { get; private set; } + public string DigestUri { get; private set; } + public string Response { get; private set; } + public int? MaxBuf { get; private set; } + public string Charset { get; private set; } + public string Algorithm { get; private set; } + public string Cipher { get; private set; } + public string AuthZid { get; private set; } + + public DigestResponse (DigestChallenge challenge, Encoding encoding, string protocol, string hostName, string authzid, string userName, string password, string cnonce) + { + UserName = userName; + + if (challenge.Realms != null && challenge.Realms.Length > 0) + Realm = challenge.Realms[0]; + else + Realm = string.Empty; + + Nonce = challenge.Nonce; + CNonce = cnonce; + Nc = 1; + + // FIXME: make sure this is supported + Qop = "auth"; + + DigestUri = string.Format ("{0}/{1}", protocol, hostName); + Algorithm = challenge.Algorithm; + Charset = challenge.Charset; + MaxBuf = challenge.MaxBuf; + AuthZid = authzid; + Cipher = null; + + Response = ComputeHash (encoding, password, true); + } + + static string HexEncode (byte[] digest) + { + var hex = new StringBuilder (); + + for (int i = 0; i < digest.Length; i++) + hex.Append (digest[i].ToString ("x2")); + + return hex.ToString (); + } + + public string ComputeHash (Encoding encoding, string password, bool client) + { + string text, a1, a2; + byte[] buf, digest; + + // compute A1 + text = string.Format ("{0}:{1}:{2}", UserName, Realm, password); + buf = encoding.GetBytes (text); + using (var md5 = MD5.Create ()) + digest = md5.ComputeHash (buf); + + using (var md5 = MD5.Create ()) { + md5.TransformBlock (digest, 0, digest.Length, null, 0); + text = string.Format (":{0}:{1}", Nonce, CNonce); + if (!string.IsNullOrEmpty (AuthZid)) + text += ":" + AuthZid; + buf = encoding.GetBytes (text); + md5.TransformFinalBlock (buf, 0, buf.Length); + a1 = HexEncode (md5.Hash); + } + + // compute A2 + text = client ? "AUTHENTICATE:" : ":"; + text += DigestUri; + + if (Qop == "auth-int" || Qop == "auth-conf") + text += ":00000000000000000000000000000000"; + + buf = encoding.GetBytes (text); + using (var md5 = MD5.Create ()) + digest = md5.ComputeHash (buf); + a2 = HexEncode (digest); + + // compute KD + text = string.Format ("{0}:{1}:{2:x8}:{3}:{4}:{5}", a1, Nonce, Nc, CNonce, Qop, a2); + buf = encoding.GetBytes (text); + using (var md5 = MD5.Create ()) + digest = md5.ComputeHash (buf); + + return HexEncode (digest); + } + + static string Quote (string text) + { + var quoted = new StringBuilder (); + + quoted.Append ("\""); + for (int i = 0; i < text.Length; i++) { + if (text[i] == '\\' || text[i] == '"') + quoted.Append ('\\'); + quoted.Append (text[i]); + } + quoted.Append ("\""); + + return quoted.ToString (); + } + + public byte[] Encode (Encoding encoding) + { + var builder = new StringBuilder (); + builder.AppendFormat ("username={0}", Quote (UserName)); + builder.AppendFormat (",realm=\"{0}\"", Realm); + builder.AppendFormat (",nonce=\"{0}\"", Nonce); + builder.AppendFormat (",cnonce=\"{0}\"", CNonce); + builder.AppendFormat (",nc={0:x8}", Nc); + builder.AppendFormat (",qop=\"{0}\"", Qop); + builder.AppendFormat (",digest-uri=\"{0}\"", DigestUri); + builder.AppendFormat (",response={0}", Response); + if (MaxBuf.HasValue) + builder.AppendFormat (CultureInfo.InvariantCulture, ",maxbuf={0}", MaxBuf.Value); + if (!string.IsNullOrEmpty (Charset)) + builder.AppendFormat (",charset={0}", Charset); + if (!string.IsNullOrEmpty (Algorithm)) + builder.AppendFormat (",algorithm={0}", Algorithm); + if (!string.IsNullOrEmpty (Cipher)) + builder.AppendFormat (",cipher=\"{0}\"", Cipher); + if (!string.IsNullOrEmpty (AuthZid)) + builder.AppendFormat (",authzid=\"{0}\"", AuthZid); + + return encoding.GetBytes (builder.ToString ()); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismLogin.cs b/src/MailKit/Security/SaslMechanismLogin.cs new file mode 100644 index 0000000..4fc055a --- /dev/null +++ b/src/MailKit/Security/SaslMechanismLogin.cs @@ -0,0 +1,297 @@ +// +// SaslMechanismLogin.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; + +namespace MailKit.Security { + /// + /// The LOGIN SASL mechanism. + /// + /// + /// The LOGIN SASL mechanism provides little protection over the use + /// of plain-text passwords by obscuring the user name and password within + /// individual base64-encoded blobs and should be avoided unless used in + /// combination with an SSL or TLS connection. + /// + public class SaslMechanismLogin : SaslMechanism + { + enum LoginState { + UserName, + Password + } + + readonly Encoding encoding; + LoginState state; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The URI of the service. + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismLogin(Encoding, NetworkCredential) instead.")] + public SaslMechanismLogin (Uri uri, Encoding encoding, ICredentials credentials) : base (uri, credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The URI of the service. + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismLogin(Encoding, string, string) instead.")] + public SaslMechanismLogin (Uri uri, Encoding encoding, string userName, string password) : base (uri, userName, password) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismLogin(NetworkCredential) instead.")] + public SaslMechanismLogin (Uri uri, ICredentials credentials) : this (uri, Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismLogin(string, string) instead.")] + public SaslMechanismLogin (Uri uri, string userName, string password) : this (uri, Encoding.UTF8, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismLogin (Encoding encoding, NetworkCredential credentials) : base (credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismLogin (Encoding encoding, string userName, string password) : base (userName, password) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismLogin (NetworkCredential credentials) : this (Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new LOGIN SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismLogin (string userName, string password) : this (Encoding.UTF8, userName, password) + { + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "LOGIN"; } + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public override bool SupportsInitialResponse { + get { return false; } + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// The SASL mechanism does not support SASL-IR. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + byte[] challenge = null; + + switch (state) { + case LoginState.UserName: + if (token == null) + throw new NotSupportedException ("LOGIN does not support SASL-IR."); + + challenge = encoding.GetBytes (Credentials.UserName); + state = LoginState.Password; + break; + case LoginState.Password: + challenge = encoding.GetBytes (Credentials.Password); + IsAuthenticated = true; + break; + } + + return challenge; + } + + /// + /// Resets the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public override void Reset () + { + state = LoginState.UserName; + base.Reset (); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismNtlm.cs b/src/MailKit/Security/SaslMechanismNtlm.cs new file mode 100644 index 0000000..1e7af8d --- /dev/null +++ b/src/MailKit/Security/SaslMechanismNtlm.cs @@ -0,0 +1,230 @@ +// +// SaslMechanismNtlm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; + +using MailKit.Security.Ntlm; + +namespace MailKit.Security { + /// + /// The NTLM SASL mechanism. + /// + /// + /// A SASL mechanism based on NTLM. + /// + public class SaslMechanismNtlm : SaslMechanism + { + enum LoginState { + Initial, + Challenge + } + + LoginState state; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismNtlm(NetworkCredential) instead.")] + public SaslMechanismNtlm (Uri uri, ICredentials credentials) : base (uri, credentials) + { + Level = NtlmAuthLevel.NTLMv2_only; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismNtlm(string, string) instead.")] + public SaslMechanismNtlm (Uri uri, string userName, string password) : base (uri, userName, password) + { + Level = NtlmAuthLevel.NTLMv2_only; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismNtlm (NetworkCredential credentials) : base (credentials) + { + Level = NtlmAuthLevel.NTLMv2_only; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new NTLM SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismNtlm (string userName, string password) : base (userName, password) + { + Level = NtlmAuthLevel.NTLMv2_only; + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "NTLM"; } + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public override bool SupportsInitialResponse { + get { return true; } + } + + internal NtlmAuthLevel Level { + get; set; + } + + /// + /// Gets or sets the workstation name to use for authentication. + /// + /// + /// Gets or sets the workstation name to use for authentication. + /// + /// The workstation name. + public string Workstation { + get; set; + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + string userName = Credentials.UserName; + string domain = Credentials.Domain; + MessageBase message = null; + + if (string.IsNullOrEmpty (domain)) { + int index = userName.IndexOf ('\\'); + if (index == -1) + index = userName.IndexOf ('/'); + + if (index >= 0) { + domain = userName.Substring (0, index); + userName = userName.Substring (index + 1); + } + } + + switch (state) { + case LoginState.Initial: + message = new Type1Message (Workstation, domain); + state = LoginState.Challenge; + break; + case LoginState.Challenge: + var password = Credentials.Password ?? string.Empty; + message = GetChallengeResponse (userName, password, token, startIndex, length); + IsAuthenticated = true; + break; + } + + return message?.Encode (); + } + + MessageBase GetChallengeResponse (string userName, string password, byte[] token, int startIndex, int length) + { + var type2 = new Type2Message (token, startIndex, length); + + return new Type3Message (type2, Level, userName, password, Workstation); + } + + /// + /// Resets the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public override void Reset () + { + state = LoginState.Initial; + base.Reset (); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismOAuth2.cs b/src/MailKit/Security/SaslMechanismOAuth2.cs new file mode 100644 index 0000000..ff8dc19 --- /dev/null +++ b/src/MailKit/Security/SaslMechanismOAuth2.cs @@ -0,0 +1,179 @@ +// +// SaslMechanismOAuth2.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; + +namespace MailKit.Security { + /// + /// The OAuth2 SASL mechanism. + /// + /// + /// A SASL mechanism used by Google that makes use of a short-lived + /// OAuth 2.0 access token. + /// + public class SaslMechanismOAuth2 : SaslMechanism + { + const string AuthBearer = "auth=Bearer "; + const string UserEquals = "user="; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new XOAUTH2 SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismOAuth2(NetworkCredential) instead.")] + public SaslMechanismOAuth2 (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new XOAUTH2 SASL context. + /// + /// The URI of the service. + /// The user name. + /// The auth token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismOAuth2(string, string) instead.")] + public SaslMechanismOAuth2 (Uri uri, string userName, string auth_token) : base (uri, userName, auth_token) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new XOAUTH2 SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismOAuth2 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new XOAUTH2 SASL context. + /// + /// The user name. + /// The auth token. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismOAuth2 (string userName, string auth_token) : base (userName, auth_token) + { + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "XOAUTH2"; } + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public override bool SupportsInitialResponse { + get { return true; } + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + var authToken = Credentials.Password; + var userName = Credentials.UserName; + int index = 0; + + var buf = new byte[UserEquals.Length + userName.Length + AuthBearer.Length + authToken.Length + 3]; + for (int i = 0; i < UserEquals.Length; i++) + buf[index++] = (byte) UserEquals[i]; + for (int i = 0; i < userName.Length; i++) + buf[index++] = (byte) userName[i]; + buf[index++] = 1; + for (int i = 0; i < AuthBearer.Length; i++) + buf[index++] = (byte) AuthBearer[i]; + for (int i = 0; i < authToken.Length; i++) + buf[index++] = (byte) authToken[i]; + buf[index++] = 1; + buf[index++] = 1; + + IsAuthenticated = true; + + return buf; + } + } +} diff --git a/src/MailKit/Security/SaslMechanismPlain.cs b/src/MailKit/Security/SaslMechanismPlain.cs new file mode 100644 index 0000000..c37ccfa --- /dev/null +++ b/src/MailKit/Security/SaslMechanismPlain.cs @@ -0,0 +1,293 @@ +// +// SaslMechanismPlain.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; + +namespace MailKit.Security { + /// + /// The PLAIN SASL mechanism. + /// + /// + /// The PLAIN SASL mechanism provides little protection over the use + /// of plain-text passwords by combining the user name and password and + /// obscuring them within a base64-encoded blob and should be avoided + /// unless used in combination with an SSL or TLS connection. + /// + public class SaslMechanismPlain : SaslMechanism + { + readonly Encoding encoding; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The URI of the service. + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismPlain(Encoding, NetworkCredential) instead.")] + public SaslMechanismPlain (Uri uri, Encoding encoding, ICredentials credentials) : base (uri, credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The URI of the service. + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismPlain(Encoding, string, string) instead.")] + public SaslMechanismPlain (Uri uri, Encoding encoding, string userName, string password) : base (uri, userName, password) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismPlain(NetworkCredential) instead.")] + public SaslMechanismPlain (Uri uri, ICredentials credentials) : this (uri, Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismPlain(string, string) instead.")] + public SaslMechanismPlain (Uri uri, string userName, string password) : this (uri, Encoding.UTF8, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismPlain (Encoding encoding, NetworkCredential credentials) : base (credentials) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The encoding to use for the user's credentials. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismPlain (Encoding encoding, string userName, string password) : base (userName, password) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + this.encoding = encoding; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismPlain (NetworkCredential credentials) : this (Encoding.UTF8, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new PLAIN SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismPlain (string userName, string password) : this (Encoding.UTF8, userName, password) + { + } + + /// + /// Gets or sets the authorization identifier. + /// + /// + /// The authorization identifier is the desired user account that the server should use + /// for all accesses. This is separate from the user name used for authentication. + /// + /// The authorization identifier. + public string AuthorizationId { + get; set; + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "PLAIN"; } + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public override bool SupportsInitialResponse { + get { return true; } + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + var authzid = encoding.GetBytes (AuthorizationId ?? string.Empty); + var authcid = encoding.GetBytes (Credentials.UserName); + var passwd = encoding.GetBytes (Credentials.Password); + var buffer = new byte[authzid.Length + authcid.Length + passwd.Length + 2]; + int offset = 0; + + for (int i = 0; i < authzid.Length; i++) + buffer[offset++] = authzid[i]; + + buffer[offset++] = 0; + for (int i = 0; i < authcid.Length; i++) + buffer[offset++] = authcid[i]; + + buffer[offset++] = 0; + for (int i = 0; i < passwd.Length; i++) + buffer[offset++] = passwd[i]; + + Array.Clear (passwd, 0, passwd.Length); + + IsAuthenticated = true; + + return buffer; + } + } +} diff --git a/src/MailKit/Security/SaslMechanismScramBase.cs b/src/MailKit/Security/SaslMechanismScramBase.cs new file mode 100644 index 0000000..60d87fc --- /dev/null +++ b/src/MailKit/Security/SaslMechanismScramBase.cs @@ -0,0 +1,376 @@ +// +// SaslMechanismScramBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Text; +using System.Globalization; +using System.Collections.Generic; +using System.Security.Cryptography; + +namespace MailKit.Security { + /// + /// The base class for SCRAM-based SASL mechanisms. + /// + /// + /// SCRAM-based SASL mechanisms are salted challenge/response authentication mechanisms. + /// + public abstract class SaslMechanismScramBase : SaslMechanism + { + enum LoginState { + Initial, + Final, + Validate + } + + internal string cnonce; + string client, server; + byte[] salted, auth; + LoginState state; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-based SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramBase(NetworkCredential) instead.")] + protected SaslMechanismScramBase (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-based SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramBase(string, string) instead.")] + protected SaslMechanismScramBase (Uri uri, string userName, string password) : base (uri, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-based SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + protected SaslMechanismScramBase (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-based SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + protected SaslMechanismScramBase (string userName, string password) : base (userName, password) + { + } + + /// + /// Gets whether or not the mechanism supports an initial response (SASL-IR). + /// + /// + /// SASL mechanisms that support sending an initial client response to the server + /// should return true. + /// + /// true if the mechanism supports an initial response; otherwise, false. + public override bool SupportsInitialResponse { + get { return true; } + } + + static string Normalize (string str) + { + var builder = new StringBuilder (); + var prepared = SaslPrep (str); + + for (int i = 0; i < prepared.Length; i++) { + switch (prepared[i]) { + case ',': builder.Append ("=2C"); break; + case '=': builder.Append ("=3D"); break; + default: + builder.Append (prepared[i]); + break; + } + } + + return builder.ToString (); + } + + /// + /// Create the HMAC context. + /// + /// + /// Creates the HMAC context using the secret key. + /// + /// The HMAC context. + /// The secret key. + protected abstract KeyedHashAlgorithm CreateHMAC (byte[] key); + + /// + /// Apply the HMAC keyed algorithm. + /// + /// + /// HMAC(key, str): Apply the HMAC keyed hash algorithm (defined in + /// [RFC2104]) using the octet string represented by "key" as the key + /// and the octet string "str" as the input string. The size of the + /// result is the hash result size for the hash function in use. For + /// example, it is 20 octets for SHA-1 (see [RFC3174]). + /// + /// The results of the HMAC keyed algorithm. + /// The key. + /// The string. + byte[] HMAC (byte[] key, byte[] str) + { + using (var hmac = CreateHMAC (key)) + return hmac.ComputeHash (str); + } + + /// + /// Apply the cryptographic hash function. + /// + /// + /// H(str): Apply the cryptographic hash function to the octet string + /// "str", producing an octet string as a result. The size of the + /// result depends on the hash result size for the hash function in + /// use. + /// + /// The results of the hash. + /// The string. + protected abstract byte[] Hash (byte[] str); + + /// + /// Apply the exclusive-or operation to combine two octet strings. + /// + /// + /// Apply the exclusive-or operation to combine the octet string + /// on the left of this operator with the octet string on the right of + /// this operator. The length of the output and each of the two + /// inputs will be the same for this use. + /// + /// The alpha component. + /// The blue component. + static void Xor (byte[] a, byte[] b) + { + for (int i = 0; i < a.Length; i++) + a[i] = (byte) (a[i] ^ b[i]); + } + + // Hi(str, salt, i): + // + // U1 := HMAC(str, salt + INT(1)) + // U2 := HMAC(str, U1) + // ... + // Ui-1 := HMAC(str, Ui-2) + // Ui := HMAC(str, Ui-1) + // + // Hi := U1 XOR U2 XOR ... XOR Ui + // + // where "i" is the iteration count, "+" is the string concatenation + // operator, and INT(g) is a 4-octet encoding of the integer g, most + // significant octet first. + // + // Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + // pseudorandom function (PRF) and with dkLen == output length of + // HMAC() == output length of H(). + byte[] Hi (byte[] str, byte[] salt, int count) + { + using (var hmac = CreateHMAC (str)) { + var salt1 = new byte[salt.Length + 4]; + byte[] hi, u1; + + Buffer.BlockCopy (salt, 0, salt1, 0, salt.Length); + salt1[salt1.Length - 1] = (byte) 1; + + hi = u1 = hmac.ComputeHash (salt1); + + for (int i = 1; i < count; i++) { + var u2 = hmac.ComputeHash (u1); + Xor (hi, u2); + u1 = u2; + } + + return hi; + } + } + + static Dictionary ParseServerChallenge (string challenge) + { + var results = new Dictionary (); + + foreach (var pair in challenge.Split (',')) { + if (pair.Length < 2 || pair[1] != '=') + continue; + + results.Add (pair[0], pair.Substring (2)); + } + + return results; + } + + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// + /// Parses the server's challenge token and returns the next challenge response. + /// + /// The next challenge response. + /// The server's challenge token. + /// The index into the token specifying where the server's challenge begins. + /// The length of the server's challenge. + /// + /// The SASL mechanism is already authenticated. + /// + /// + /// An error has occurred while parsing the server's challenge token. + /// + protected override byte[] Challenge (byte[] token, int startIndex, int length) + { + if (IsAuthenticated) + throw new InvalidOperationException (); + + byte[] response, signature; + + switch (state) { + case LoginState.Initial: + cnonce = cnonce ?? GenerateEntropy (18); + client = "n=" + Normalize (Credentials.UserName) + ",r=" + cnonce; + response = Encoding.UTF8.GetBytes ("n,," + client); + state = LoginState.Final; + break; + case LoginState.Final: + server = Encoding.UTF8.GetString (token, startIndex, length); + var tokens = ParseServerChallenge (server); + string salt, nonce, iterations; + int count; + + if (!tokens.TryGetValue ('s', out salt)) + throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a salt."); + + if (!tokens.TryGetValue ('r', out nonce)) + throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a nonce."); + + if (!tokens.TryGetValue ('i', out iterations)) + throw new SaslException (MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain an iteration count."); + + if (!nonce.StartsWith (cnonce, StringComparison.Ordinal)) + throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid nonce."); + + if (!int.TryParse (iterations, NumberStyles.None, CultureInfo.InvariantCulture, out count) || count < 1) + throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid iteration count."); + + var password = Encoding.UTF8.GetBytes (SaslPrep (Credentials.Password)); + salted = Hi (password, Convert.FromBase64String (salt), count); + Array.Clear (password, 0, password.Length); + + var withoutProof = "c=" + Convert.ToBase64String (Encoding.ASCII.GetBytes ("n,,")) + ",r=" + nonce; + auth = Encoding.UTF8.GetBytes (client + "," + server + "," + withoutProof); + + var key = HMAC (salted, Encoding.ASCII.GetBytes ("Client Key")); + signature = HMAC (Hash (key), auth); + Xor (key, signature); + + response = Encoding.UTF8.GetBytes (withoutProof + ",p=" + Convert.ToBase64String (key)); + state = LoginState.Validate; + break; + case LoginState.Validate: + var challenge = Encoding.UTF8.GetString (token, startIndex, length); + + if (!challenge.StartsWith ("v=", StringComparison.Ordinal)) + throw new SaslException (MechanismName, SaslErrorCode.InvalidChallenge, "Challenge did not start with a signature."); + + signature = Convert.FromBase64String (challenge.Substring (2)); + var serverKey = HMAC (salted, Encoding.ASCII.GetBytes ("Server Key")); + var calculated = HMAC (serverKey, auth); + + if (signature.Length != calculated.Length) + throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained a signature with an invalid length."); + + for (int i = 0; i < signature.Length; i++) { + if (signature[i] != calculated[i]) + throw new SaslException (MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained an invalid signatire."); + } + + IsAuthenticated = true; + response = new byte[0]; + break; + default: + throw new IndexOutOfRangeException ("state"); + } + + return response; + } + + /// + /// Resets the state of the SASL mechanism. + /// + /// + /// Resets the state of the SASL mechanism. + /// + public override void Reset () + { + state = LoginState.Initial; + client = null; + server = null; + salted = null; + cnonce = null; + auth = null; + + base.Reset (); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismScramSha1.cs b/src/MailKit/Security/SaslMechanismScramSha1.cs new file mode 100644 index 0000000..706186f --- /dev/null +++ b/src/MailKit/Security/SaslMechanismScramSha1.cs @@ -0,0 +1,151 @@ +// +// SaslMechanismScramSha1.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Security.Cryptography; + +namespace MailKit.Security { + /// + /// The SCRAM-SHA-1 SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-1 algorithm. + /// + public class SaslMechanismScramSha1 : SaslMechanismScramBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1 SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramSha1(NetworkCredential) instead.")] + public SaslMechanismScramSha1 (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1 SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramSha1(string, string) instead.")] + public SaslMechanismScramSha1 (Uri uri, string userName, string password) : base (uri, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1 SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismScramSha1 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-1 SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismScramSha1 (string userName, string password) : base (userName, password) + { + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-1"; } + } + + /// + /// Create the HMAC context. + /// + /// + /// Creates the HMAC context using the secret key. + /// + /// The HMAC context. + /// The secret key. + protected override KeyedHashAlgorithm CreateHMAC (byte[] key) + { + return new HMACSHA1 (key); + } + + /// + /// Apply the cryptographic hash function. + /// + /// + /// H(str): Apply the cryptographic hash function to the octet string + /// "str", producing an octet string as a result. The size of the + /// result depends on the hash result size for the hash function in + /// use. + /// + /// The results of the hash. + /// The string. + protected override byte[] Hash (byte[] str) + { + using (var sha1 = SHA1.Create ()) + return sha1.ComputeHash (str); + } + } +} diff --git a/src/MailKit/Security/SaslMechanismScramSha256.cs b/src/MailKit/Security/SaslMechanismScramSha256.cs new file mode 100644 index 0000000..f53ec0a --- /dev/null +++ b/src/MailKit/Security/SaslMechanismScramSha256.cs @@ -0,0 +1,151 @@ +// +// SaslMechanismScramSha256.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Net; +using System.Security.Cryptography; + +namespace MailKit.Security { + /// + /// The SCRAM-SHA-256 SASL mechanism. + /// + /// + /// A salted challenge/response SASL mechanism that uses the HMAC SHA-256 algorithm. + /// + public class SaslMechanismScramSha256 : SaslMechanismScramBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256 SASL context. + /// + /// The URI of the service. + /// The user's credentials. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramSha256(NetworkCredential) instead.")] + public SaslMechanismScramSha256 (Uri uri, ICredentials credentials) : base (uri, credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256 SASL context. + /// + /// The URI of the service. + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use SaslMechanismScramSha256(string, string) instead.")] + public SaslMechanismScramSha256 (Uri uri, string userName, string password) : base (uri, userName, password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256 SASL context. + /// + /// The user's credentials. + /// + /// is null. + /// + public SaslMechanismScramSha256 (NetworkCredential credentials) : base (credentials) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new SCRAM-SHA-256 SASL context. + /// + /// The user name. + /// The password. + /// + /// is null. + /// -or- + /// is null. + /// + public SaslMechanismScramSha256 (string userName, string password) : base (userName, password) + { + } + + /// + /// Gets the name of the mechanism. + /// + /// + /// Gets the name of the mechanism. + /// + /// The name of the mechanism. + public override string MechanismName { + get { return "SCRAM-SHA-256"; } + } + + /// + /// Create the HMAC context. + /// + /// + /// Creates the HMAC context using the secret key. + /// + /// The HMAC context. + /// The secret key. + protected override KeyedHashAlgorithm CreateHMAC (byte[] key) + { + return new HMACSHA256 (key); + } + + /// + /// Apply the cryptographic hash function. + /// + /// + /// H(str): Apply the cryptographic hash function to the octet string + /// "str", producing an octet string as a result. The size of the + /// result depends on the hash result size for the hash function in + /// use. + /// + /// The results of the hash. + /// The string. + protected override byte[] Hash (byte[] str) + { + using (var sha256 = SHA256.Create ()) + return sha256.ComputeHash (str); + } + } +} diff --git a/src/MailKit/Security/SecureSocketOptions.cs b/src/MailKit/Security/SecureSocketOptions.cs new file mode 100644 index 0000000..a30205d --- /dev/null +++ b/src/MailKit/Security/SecureSocketOptions.cs @@ -0,0 +1,68 @@ +// +// SecureSocketOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit.Security { + /// + /// Secure socket options. + /// + /// + /// Provides a way of specifying the SSL and/or TLS encryption that + /// should be used for a connection. + /// + public enum SecureSocketOptions { + /// + /// No SSL or TLS encryption should be used. + /// + None, + + /// + /// Allow the to decide which SSL or TLS + /// options to use (default). If the server does not support SSL or TLS, + /// then the connection will continue without any encryption. + /// + Auto, + + /// + /// The connection should use SSL or TLS encryption immediately. + /// + SslOnConnect, + + /// + /// Elevates the connection to use TLS encryption immediately after + /// reading the greeting and capabilities of the server. If the server + /// does not support the STARTTLS extension, then the connection will + /// fail and a will be thrown. + /// + StartTls, + + /// + /// Elevates the connection to use TLS encryption immediately after + /// reading the greeting and capabilities of the server, but only if + /// the server supports the STARTTLS extension. + /// + StartTlsWhenAvailable, + } +} diff --git a/src/MailKit/Security/SslHandshakeException.cs b/src/MailKit/Security/SslHandshakeException.cs new file mode 100644 index 0000000..5a3c0b5 --- /dev/null +++ b/src/MailKit/Security/SslHandshakeException.cs @@ -0,0 +1,305 @@ +// +// SslHandshakeException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Net.Security; +using System.Collections.Generic; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif +using System.Security.Cryptography.X509Certificates; + +namespace MailKit.Security +{ + /// + /// The exception that is thrown when there is an error during the SSL/TLS handshake. + /// + /// + /// The exception that is thrown when there is an error during the SSL/TLS handshake. + /// When this exception occurrs, it typically means that the IMAP, POP3 or SMTP server that + /// you are connecting to is using an SSL certificate that is either expired or untrusted by + /// your system. + /// Often times, mail servers will use self-signed certificates instead of using a certificate + /// that has been signed by a trusted Certificate Authority. When your system is unable to validate + /// the mail server's certificate because it is not signed by a known and trusted Certificate Authority, + /// this exception will occur. + /// You can work around this problem by supplying a custom + /// and setting it on the client's property. + /// Most likely, you'll want to compare the thumbprint of the server's certificate with a known + /// value and/or prompt the user to accept the certificate (similar to what you've probably seen web + /// browsers do when they encounter untrusted certificates). + /// +#if SERIALIZABLE + [Serializable] +#endif + public class SslHandshakeException : Exception + { + const string SslHandshakeHelpLink = "https://github.com/jstedfast/MailKit/blob/master/FAQ.md#SslHandshakeException"; + const string DefaultMessage = "An error occurred while attempting to establish an SSL or TLS connection."; + +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new from the seriaized data. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + protected SslHandshakeException (SerializationInfo info, StreamingContext context) : base (info, context) + { + var base64 = info.GetString ("ServerCertificate"); + + if (base64 != null) + ServerCertificate = new X509Certificate2 (Convert.FromBase64String (base64)); + + base64 = info.GetString ("RootCertificateAuthority"); + + if (base64 != null) + RootCertificateAuthority = new X509Certificate2 (Convert.FromBase64String (base64)); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// An inner exception. + public SslHandshakeException (string message, Exception innerException) : base (message, innerException) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public SslHandshakeException (string message) : base (message) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public SslHandshakeException () : base (DefaultMessage) + { + HelpLink = SslHandshakeHelpLink; + } + + /// + /// Get the server's SSL certificate. + /// + /// + /// Gets the server's SSL certificate, if it is available. + /// + /// The server's SSL certificate. + public X509Certificate ServerCertificate { + get; private set; + } + + /// + /// Get the certificate for the Root Certificate Authority. + /// + /// + /// Gets the certificate for the Root Certificate Authority, if it is available. + /// + /// The Root Certificate Authority certificate. + public X509Certificate RootCertificateAuthority { + get; private set; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + if (ServerCertificate != null) + info.AddValue ("ServerCertificate", Convert.ToBase64String (ServerCertificate.GetRawCertData ())); + else + info.AddValue ("ServerCertificate", null, typeof (string)); + + if (RootCertificateAuthority != null) + info.AddValue ("RootCertificateAuthority", Convert.ToBase64String (RootCertificateAuthority.GetRawCertData ())); + else + info.AddValue ("RootCertificateAuthority", null, typeof (string)); + } +#endif + + internal static SslHandshakeException Create (MailService client, Exception ex, bool starttls) + { + var message = new StringBuilder (DefaultMessage); + var aggregate = ex as AggregateException; + X509Certificate certificate = null; + X509Certificate root = null; + + if (aggregate != null) { + aggregate = aggregate.Flatten (); + + if (aggregate.InnerExceptions.Count == 1) + ex = aggregate.InnerExceptions[0]; + else + ex = aggregate; + } + + message.AppendLine (); + message.AppendLine (); + + var validationInfo = client?.SslCertificateValidationInfo; + if (validationInfo != null) { + client.SslCertificateValidationInfo = null; + + int rootIndex = validationInfo.ChainElements.Count - 1; + if (rootIndex > 0) + root = validationInfo.ChainElements[rootIndex].Certificate; + certificate = validationInfo.Certificate; + + if ((validationInfo.SslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) { + message.AppendLine ("The SSL certificate for the server was not available."); + } else if ((validationInfo.SslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) { + message.AppendLine ("The host name did not match the name given in the server's SSL certificate."); + } else { + message.AppendLine ("The server's SSL certificate could not be validated for the following reasons:"); + + bool haveReason = false; + + for (int chainIndex = 0; chainIndex < validationInfo.ChainElements.Count; chainIndex++) { + var element = validationInfo.ChainElements[chainIndex]; + + if (element.ChainElementStatus == null || element.ChainElementStatus.Length == 0) + continue; + + if (chainIndex == 0) { + message.AppendLine ("\u2022 The server certificate has the following errors:"); + } else if (chainIndex == rootIndex) { + message.AppendLine ("\u2022 The root certificate has the following errors:"); + } else { + message.AppendLine ("\u2022 An intermediate certificate has the following errors:"); + } + + foreach (var status in element.ChainElementStatus) + message.AppendFormat (" \u2022 {0}{1}", status.StatusInformation, Environment.NewLine); + + haveReason = true; + } + + // Note: Because Mono does not include any elements in the chain (at least on macOS), we need + // to find the inner-most exception and append its Message. + if (!haveReason) { + var innerException = ex; + + while (innerException.InnerException != null) + innerException = innerException.InnerException; + + message.AppendLine ("\u2022 " + innerException.Message); + } + } + } else { + message.AppendLine ("This usually means that the SSL certificate presented by the server is not trusted by the system for one or more of"); + message.AppendLine ("the following reasons:"); + message.AppendLine (); + message.AppendLine ("1. The server is using a self-signed certificate which cannot be verified."); + message.AppendLine ("2. The local system is missing a Root or Intermediate certificate needed to verify the server's certificate."); + message.AppendLine ("3. A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable."); + message.AppendLine ("4. The certificate presented by the server is expired or invalid."); + message.AppendLine (); + if (!starttls) { + message.AppendLine ("Another possibility is that you are trying to connect to a port which does not support SSL/TLS."); + message.AppendLine (); + } + message.AppendLine ("It is also possible that the set of SSL/TLS protocols supported by the client and server do not match."); + message.AppendLine (); + message.AppendLine ("See " + SslHandshakeHelpLink + " for possible solutions."); + } + + return new SslHandshakeException (message.ToString (), ex) { ServerCertificate = certificate, RootCertificateAuthority = root }; + } + } + + class SslChainElement + { + public readonly X509Certificate Certificate; + public readonly X509ChainStatus[] ChainElementStatus; + public readonly string Information; + + public SslChainElement (X509ChainElement element) + { + Certificate = new X509Certificate2 (element.Certificate.RawData); + ChainElementStatus = element.ChainElementStatus; + Information = element.Information; + } + } + + class SslCertificateValidationInfo + { + public readonly List ChainElements; + public readonly X509ChainStatus[] ChainStatus; + public readonly SslPolicyErrors SslPolicyErrors; + public readonly X509Certificate Certificate; + public readonly string Host; + + public SslCertificateValidationInfo (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + Certificate = new X509Certificate2 (certificate.Export (X509ContentType.Cert)); + ChainElements = new List (); + SslPolicyErrors = sslPolicyErrors; + ChainStatus = chain.ChainStatus; + Host = sender as string; + + // Note: we need to copy the ChainElements because the chain will be destroyed + foreach (var element in chain.ChainElements) + ChainElements.Add (new SslChainElement (element)); + } + } +} diff --git a/src/MailKit/ServiceNotAuthenticatedException.cs b/src/MailKit/ServiceNotAuthenticatedException.cs new file mode 100644 index 0000000..a3b032b --- /dev/null +++ b/src/MailKit/ServiceNotAuthenticatedException.cs @@ -0,0 +1,97 @@ +// +// ServiceNotAuthenticatedException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when the is not authenticated. + /// + /// + /// This exception is thrown when an operation on a service could not be completed + /// due to the service not being authenticated. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ServiceNotAuthenticatedException : InvalidOperationException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected ServiceNotAuthenticatedException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The inner exception. + public ServiceNotAuthenticatedException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public ServiceNotAuthenticatedException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public ServiceNotAuthenticatedException () + { + } + } +} diff --git a/src/MailKit/ServiceNotConnectedException.cs b/src/MailKit/ServiceNotConnectedException.cs new file mode 100644 index 0000000..b21e76f --- /dev/null +++ b/src/MailKit/ServiceNotConnectedException.cs @@ -0,0 +1,97 @@ +// +// ServiceNotConnectedException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MailKit { + /// + /// The exception that is thrown when the is not connected. + /// + /// + /// This exception is thrown when an operation on a service could not be completed + /// due to the service not being connected. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ServiceNotConnectedException : InvalidOperationException + { +#if SERIALIZABLE + /// + /// Initializes a new instance of the class. + /// + /// + /// Deserializes a . + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecuritySafeCritical] + protected ServiceNotConnectedException (SerializationInfo info, StreamingContext context) : base (info, context) + { + } +#endif + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The inner exception. + public ServiceNotConnectedException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public ServiceNotConnectedException (string message) : base (message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + public ServiceNotConnectedException () + { + } + } +} diff --git a/src/MailKit/SpecialFolder.cs b/src/MailKit/SpecialFolder.cs new file mode 100644 index 0000000..f746ebb --- /dev/null +++ b/src/MailKit/SpecialFolder.cs @@ -0,0 +1,75 @@ +// +// SpecialFolder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// An enumeration of special folders. + /// + /// + /// An enumeration of special folders. + /// + public enum SpecialFolder { + /// + /// The special folder containing an aggregate of all messages. + /// + All, + + /// + /// The special folder that contains archived messages. + /// + Archive, + + /// + /// The special folder that contains message drafts. + /// + Drafts, + + /// + /// The special folder that contains flagged messages. + /// + Flagged, + + /// + /// The special folder that contains important messages. + /// + Important, + + /// + /// The special folder that contains spam messages. + /// + Junk, + + /// + /// The special folder that contains sent messages. + /// + Sent, + + /// + /// The special folder that contains deleted messages. + /// + Trash + } +} diff --git a/src/MailKit/StatusItems.cs b/src/MailKit/StatusItems.cs new file mode 100644 index 0000000..97f11c7 --- /dev/null +++ b/src/MailKit/StatusItems.cs @@ -0,0 +1,88 @@ +// +// StatusItems.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MailKit { + /// + /// Status items. + /// + /// + /// Used with + /// + [Flags] + public enum StatusItems { + /// + /// No status requested. + /// + None = 0, + + /// + /// Updates . + /// + Count = 1 << 0, + + /// + /// Updates . + /// + Recent = 1 << 1, + + /// + /// Updates . + /// + UidNext = 1 << 2, + + /// + /// Updates . + /// + UidValidity = 1 << 3, + + /// + /// Updates . + /// + Unread = 1 << 4, + + /// + /// Updates . + /// + HighestModSeq = 1 << 5, + + /// + /// Updates . + /// + AppendLimit = 1 << 6, + + /// + /// Updates . + /// + Size = 1 << 7, + + /// + /// Updates . + /// + MailboxId = 1 << 8, + } +} diff --git a/src/MailKit/ThreadingAlgorithm.cs b/src/MailKit/ThreadingAlgorithm.cs new file mode 100644 index 0000000..8c5e223 --- /dev/null +++ b/src/MailKit/ThreadingAlgorithm.cs @@ -0,0 +1,48 @@ +// +// ThreadingAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MailKit { + /// + /// An enumeration of threading algorithms. + /// + /// + /// A threading algorithm is used to group messages and their + /// replies together. + /// + public enum ThreadingAlgorithm { + /// + /// Thread messages based on their Subject headers. + /// + OrderedSubject, + + /// + /// Threads messages based on their References, In-Reply-To, and Message-Id headers. + /// This algorithm is far better than but is also more + /// expensive to calculate. + /// + References, + } +} diff --git a/src/MailKit/UniqueId.cs b/src/MailKit/UniqueId.cs new file mode 100644 index 0000000..1c2dd3a --- /dev/null +++ b/src/MailKit/UniqueId.cs @@ -0,0 +1,428 @@ +// +// UniqueId.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Globalization; + +namespace MailKit { + /// + /// A unique identifier. + /// + /// + /// Represents a unique identifier for messages in a . + /// + public struct UniqueId : IComparable, IEquatable + { + /// + /// The invalid value. + /// + /// + /// The invalid value. + /// + public static readonly UniqueId Invalid; + + /// + /// The minimum value. + /// + /// + /// The minimum value. + /// + public static readonly UniqueId MinValue = new UniqueId (1); + + /// + /// The maximum value. + /// + /// + /// The maximum value. + /// + public static readonly UniqueId MaxValue = new UniqueId (uint.MaxValue); + + readonly uint validity; + readonly uint id; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new with the specified validity and value. + /// + /// The uid validity. + /// The unique identifier. + /// + /// is 0. + /// + public UniqueId (uint validity, uint id) + { + if (id == 0) + throw new ArgumentOutOfRangeException (nameof (id)); + + this.validity = validity; + this.id = id; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Creates a new with the specified value. + /// + /// The unique identifier. + /// + /// is 0. + /// + public UniqueId (uint id) + { + if (id == 0) + throw new ArgumentOutOfRangeException (nameof (id)); + + this.validity = 0; + this.id = id; + } + + /// + /// Gets the identifier. + /// + /// + /// The identifier. + /// + /// The identifier. + public uint Id { + get { return id; } + } + + /// + /// Gets the validity, if non-zero. + /// + /// + /// Gets the UidValidity of the containing folder. + /// + /// The UidValidity of the containing folder. + public uint Validity { + get { return validity; } + } + + /// + /// Gets whether or not the unique identifier is valid. + /// + /// + /// Gets whether or not the unique identifier is valid. + /// + /// true if the unique identifier is valid; otherwise, false. + public bool IsValid { + get { return Id != 0; } + } + + #region IComparable implementation + + /// + /// Compares two objects. + /// + /// + /// Compares two objects. + /// + /// + /// A value less than 0 if this is less than , + /// a value of 0 if this is equal to , or + /// a value greater than 0 if this is greater than . + /// + /// The other unique identifier. + public int CompareTo (UniqueId other) + { + return Id.CompareTo (other.Id); + } + + #endregion + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (UniqueId other) + { + return other.Id == Id; + } + + #endregion + + /// + /// Determines whether two unique identifiers are equal. + /// + /// + /// Determines whether two unique identifiers are equal. + /// + /// true if and are equal; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator == (UniqueId uid1, UniqueId uid2) + { + return uid1.Id == uid2.Id; + } + + /// + /// Determines whether one unique identifier is greater than another unique identifier. + /// + /// + /// Determines whether one unique identifier is greater than another unique identifier. + /// + /// true if is greater than ; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator > (UniqueId uid1, UniqueId uid2) + { + return uid1.Id > uid2.Id; + } + + /// + /// Determines whether one unique identifier is greater than or equal to another unique identifier. + /// + /// + /// Determines whether one unique identifier is greater than or equal to another unique identifier. + /// + /// true if is greater than or equal to ; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator >= (UniqueId uid1, UniqueId uid2) + { + return uid1.Id >= uid2.Id; + } + + /// + /// Determines whether two unique identifiers are not equal. + /// + /// + /// Determines whether two unique identifiers are not equal. + /// + /// true if and are not equal; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator != (UniqueId uid1, UniqueId uid2) + { + return uid1.Id != uid2.Id; + } + + /// + /// Determines whether one unique identifier is less than another unique identifier. + /// + /// + /// Determines whether one unique identifier is less than another unique identifier. + /// + /// true if is less than ; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator < (UniqueId uid1, UniqueId uid2) + { + return uid1.Id < uid2.Id; + } + + /// + /// Determines whether one unique identifier is less than or equal to another unique identifier. + /// + /// + /// Determines whether one unique identifier is less than or equal to another unique identifier. + /// + /// true if is less than or equal to ; otherwise, false. + /// The first unique id to compare. + /// The second unique id to compare. + public static bool operator <= (UniqueId uid1, UniqueId uid2) + { + return uid1.Id <= uid2.Id; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current ; + /// otherwise, false. + public override bool Equals (object obj) + { + return obj is UniqueId && ((UniqueId) obj).Id == Id; + } + + /// + /// Serves as a hash function for a object. + /// + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode () + { + return Id.GetHashCode (); + } + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return Id.ToString (CultureInfo.InvariantCulture); + } + + /// + /// Attempt to parse a unique identifier. + /// + /// + /// Attempts to parse a unique identifier. + /// + /// true if the unique identifier was successfully parsed; otherwise, false.. + /// The token to parse. + /// The index to start parsing. + /// The unique identifier. + internal static bool TryParse (string token, ref int index, out uint uid) + { + uint value = 0; + + while (index < token.Length) { + char c = token[index]; + uint v; + + if (c < '0' || c > '9') + break; + + v = (uint) (c - '0'); + + if (value > uint.MaxValue / 10 || (value == uint.MaxValue / 10 && v > uint.MaxValue % 10)) { + uid = 0; + return false; + } + + value = (value * 10) + v; + index++; + } + + uid = value; + + return uid != 0; + } + + /// + /// Attempt to parse a unique identifier. + /// + /// + /// Attempts to parse a unique identifier. + /// + /// true if the unique identifier was successfully parsed; otherwise, false.. + /// The token to parse. + /// The UIDVALIDITY value. + /// The unique identifier. + /// + /// is null. + /// + public static bool TryParse (string token, uint validity, out UniqueId uid) + { + if (token == null) + throw new ArgumentNullException (nameof (token)); + + uint id; + + if (!uint.TryParse (token, NumberStyles.None, CultureInfo.InvariantCulture, out id) || id == 0) { + uid = Invalid; + return false; + } + + uid = new UniqueId (validity, id); + + return true; + } + + /// + /// Attempt to parse a unique identifier. + /// + /// + /// Attempts to parse a unique identifier. + /// + /// true if the unique identifier was successfully parsed; otherwise, false.. + /// The token to parse. + /// The unique identifier. + /// + /// is null. + /// + public static bool TryParse (string token, out UniqueId uid) + { + return TryParse (token, 0, out uid); + } + + /// + /// Parse a unique identifier. + /// + /// + /// Parses a unique identifier. + /// + /// The unique identifier. + /// A string containing the unique identifier. + /// The UIDVALIDITY. + /// + /// is null. + /// + /// + /// is not in the correct format. + /// + /// + /// The unique identifier is greater than . + /// + public static UniqueId Parse (string token, uint validity) + { + return new UniqueId (validity, uint.Parse (token, NumberStyles.None, CultureInfo.InvariantCulture)); + } + + /// + /// Parse a unique identifier. + /// + /// + /// Parses a unique identifier. + /// + /// The unique identifier. + /// A string containing the unique identifier. + /// + /// is null. + /// + /// + /// is not in the correct format. + /// + /// + /// The unique identifier is greater than . + /// + public static UniqueId Parse (string token) + { + return new UniqueId (uint.Parse (token, NumberStyles.None, CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/MailKit/UniqueIdMap.cs b/src/MailKit/UniqueIdMap.cs new file mode 100644 index 0000000..d637c33 --- /dev/null +++ b/src/MailKit/UniqueIdMap.cs @@ -0,0 +1,223 @@ +// +// UniqueIdMap.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MailKit { + /// + /// A mapping of unique identifiers. + /// + /// + /// A can be used to discover the mapping of one set of unique identifiers + /// to another. + /// For example, when copying or moving messages from one folder to another, it is often desirable + /// to know what the unique identifiers are for each of the messages in the destination folder. + /// + public class UniqueIdMap : IReadOnlyDictionary + { + /// + /// Any empty mapping of unique identifiers. + /// + /// + /// Any empty mapping of unique identifiers. + /// + public static readonly UniqueIdMap Empty = new UniqueIdMap (); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The unique identifiers used in the source folder. + /// The unique identifiers used in the destination folder. + /// + /// is null. + /// -or- + /// is null. + /// + public UniqueIdMap (IList source, IList destination) + { + if (source == null) + throw new ArgumentNullException (nameof (source)); + + if (destination == null) + throw new ArgumentNullException (nameof (destination)); + + Destination = destination; + Source = source; + } + + UniqueIdMap () + { + Destination = Source = new UniqueId[0]; + } + + /// + /// Gets the list of unique identifiers used in the source folder. + /// + /// + /// Gets the list of unique identifiers used in the source folder. + /// + /// The unique identifiers used in the source folder. + public IList Source { + get; private set; + } + + /// + /// Gets the list of unique identifiers used in the destination folder. + /// + /// + /// Gets the list of unique identifiers used in the destination folder. + /// + /// The unique identifiers used in the destination folder. + public IList Destination { + get; private set; + } + + /// + /// Gets the number of unique identifiers that have been remapped. + /// + /// + /// Gets the number of unique identifiers that have been remapped. + /// + /// The count. + public int Count { + get { return Source.Count; } + } + + /// + /// Gets the keys. + /// + /// + /// Gets the keys. + /// + /// The keys. + public IEnumerable Keys { + get { return Source; } + } + + /// + /// Gets the values. + /// + /// + /// Gets the values. + /// + /// The values. + public IEnumerable Values { + get { return Destination; } + } + + /// + /// Checks if the specified unique identifier has been remapped. + /// + /// + /// Checks if the specified unique identifier has been remapped. + /// + /// true if the unique identifier has been remapped; otherwise, false. + /// The unique identifier. + public bool ContainsKey (UniqueId key) + { + return Source.Contains (key); + } + + /// + /// Tries to get the remapped unique identifier. + /// + /// + /// Attempts to get the remapped unique identifier. + /// + /// true on success; otherwise, false. + /// The unique identifier of the message in the source folder. + /// The unique identifier of the message in the destination folder. + public bool TryGetValue (UniqueId key, out UniqueId value) + { + int index = Source.IndexOf (key); + + if (index == -1 || index >= Destination.Count) { + value = UniqueId.Invalid; + return false; + } + + value = Destination[index]; + + return true; + } + + /// + /// Gets the remapped unique identifier. + /// + /// + /// Gets the remapped unique identifier. + /// + /// The unique identifier of the message in the source folder. + /// + /// is out of range. + /// + public UniqueId this [UniqueId index] { + get { + UniqueId uid; + + if (!TryGetValue (index, out uid)) + throw new ArgumentOutOfRangeException (nameof (index)); + + return uid; + } + } + + /// + /// Gets the enumerator for the remapped unique identifiers. + /// + /// + /// Gets the enumerator for the remapped unique identifiers. + /// + /// The enumerator. + public IEnumerator> GetEnumerator () + { + var dst = Destination.GetEnumerator (); + var src = Source.GetEnumerator (); + + while (src.MoveNext () && dst.MoveNext ()) + yield return new KeyValuePair (src.Current, dst.Current); + + yield break; + } + + /// + /// Gets the enumerator for the remapped unique identifiers. + /// + /// + /// Gets the enumerator for the remapped unique identifiers. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + } +} diff --git a/src/MailKit/UniqueIdRange.cs b/src/MailKit/UniqueIdRange.cs new file mode 100644 index 0000000..244aae3 --- /dev/null +++ b/src/MailKit/UniqueIdRange.cs @@ -0,0 +1,494 @@ +// +// UniqueIdRange.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +namespace MailKit { + /// + /// A range of items. + /// + /// + /// When dealing with a large range, it is more efficient to use a + /// than a typical + /// IList<>. + /// + public class UniqueIdRange : IList + { + /// + /// A that encompases all messages in the folder. + /// + /// + /// Represents the range of messages from to + /// . + /// + public static readonly UniqueIdRange All = new UniqueIdRange (UniqueId.MinValue, UniqueId.MaxValue); + + static readonly UniqueIdRange Invalid = new UniqueIdRange (); + + readonly uint validity; + internal uint start; + internal uint end; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new (invalid) range of unique identifiers. + /// + UniqueIdRange () + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new range of unique identifiers. + /// + /// The uid validity. + /// The first unique identifier in the range. + /// The last unique identifier in the range. + public UniqueIdRange (uint validity, uint start, uint end) + { + if (start == 0) + throw new ArgumentOutOfRangeException (nameof (start)); + + if (end == 0) + throw new ArgumentOutOfRangeException (nameof (end)); + + this.validity = validity; + this.start = start; + this.end = end; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new range of unique identifiers. + /// + /// The first in the range. + /// The last in the range. + /// + /// is invalid. + /// -or- + /// is invalid. + /// + public UniqueIdRange (UniqueId start, UniqueId end) + { + if (!start.IsValid) + throw new ArgumentOutOfRangeException (nameof (start)); + + if (!end.IsValid) + throw new ArgumentOutOfRangeException (nameof (end)); + + this.validity = start.Validity; + this.start = start.Id; + this.end = end.Id; + } + + /// + /// Gets the validity, if non-zero. + /// + /// + /// Gets the UidValidity of the containing folder. + /// + /// The UidValidity of the containing folder. + public uint Validity { + get { return validity; } + } + + /// + /// Gets the minimum unique identifier in the range. + /// + /// + /// Gets the minimum unique identifier in the range. + /// + /// The minimum unique identifier. + public UniqueId Min { + get { return start < end ? new UniqueId (validity, start) : new UniqueId (validity, end); } + } + + /// + /// Gets the maximum unique identifier in the range. + /// + /// + /// Gets the maximum unique identifier in the range. + /// + /// The maximum unique identifier. + public UniqueId Max { + get { return start > end ? new UniqueId (validity, start) : new UniqueId (validity, end); } + } + + /// + /// Get the start of the unique identifier range. + /// + /// + /// Gets the start of the unique identifier range. + /// + /// The start of the range. + public UniqueId Start { + get { return new UniqueId (validity, start); } + } + + /// + /// Get the end of the unique identifier range. + /// + /// + /// Gets the end of the unique identifier range. + /// + /// The end of the range. + public UniqueId End { + get { return new UniqueId (validity, end); } + } + + #region ICollection implementation + + /// + /// Get the number of unique identifiers in the range. + /// + /// + /// Gets the number of unique identifiers in the range. + /// + /// The count. + public int Count { + get { return (int) (start <= end ? end - start : start - end) + 1; } + } + + /// + /// Get whether or not the range is read only. + /// + /// + /// A is always read-only. + /// + /// true if the range is read only; otherwise, false. + public bool IsReadOnly { + get { return true; } + } + + /// + /// Adds the unique identifier to the range. + /// + /// + /// Since a is read-only, unique ids cannot + /// be added to the range. + /// + /// The unique identifier to add. + /// + /// The list does not support adding items. + /// + public void Add (UniqueId uid) + { + throw new NotSupportedException (); + } + + /// + /// Clears the list. + /// + /// + /// Since a is read-only, the range cannot be cleared. + /// + /// + /// The list does not support being cleared. + /// + public void Clear () + { + throw new NotSupportedException (); + } + + /// + /// Checks if the range contains the specified unique id. + /// + /// + /// Determines whether or not the range contains the specified unique id. + /// + /// true if the specified unique identifier is in the range; otherwise false. + /// The unique id. + public bool Contains (UniqueId uid) + { + if (start <= end) + return uid.Id >= start && uid.Id <= end; + + return uid.Id <= start && uid.Id >= end; + } + + /// + /// Copies all of the unique ids in the range to the specified array. + /// + /// + /// Copies all of the unique ids within the range into the array, + /// starting at the specified array index. + /// + /// The array to copy the unique ids to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (UniqueId[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex > (array.Length - Count)) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + int index = arrayIndex; + + if (start <= end) { + for (uint uid = start; uid <= end; uid++, index++) + array[index] = new UniqueId (validity, uid); + } else { + for (uint uid = start; uid >= end; uid--, index++) + array[index] = new UniqueId (validity, uid); + } + } + + /// + /// Removes the unique identifier from the range. + /// + /// + /// Since a is read-only, unique ids cannot be removed. + /// + /// true if the unique identifier was removed; otherwise false. + /// The unique identifier to remove. + /// + /// The list does not support removing items. + /// + public bool Remove (UniqueId uid) + { + throw new NotSupportedException (); + } + + #endregion + + #region IList implementation + + /// + /// Gets the index of the specified unique id, if it exists. + /// + /// + /// Finds the index of the specified unique id, if it exists. + /// + /// The index of the specified unique id; otherwise -1. + /// The unique id. + public int IndexOf (UniqueId uid) + { + if (start <= end) { + if (uid.Id < start || uid.Id > end) + return -1; + + return (int) (uid.Id - start); + } + + if (uid.Id > start || uid.Id < end) + return -1; + + return (int) (start - uid.Id); + } + + /// + /// Inserts the specified unique identifier at the given index. + /// + /// + /// Inserts the unique identifier at the specified index in the range. + /// + /// The index to insert the unique id. + /// The unique id. + /// + /// The list does not support inserting items. + /// + public void Insert (int index, UniqueId uid) + { + throw new NotSupportedException (); + } + + /// + /// Removes the unique identifier at the specified index. + /// + /// + /// Removes the unique identifier at the specified index. + /// + /// The index. + /// + /// The list does not support removing items. + /// + public void RemoveAt (int index) + { + throw new NotSupportedException (); + } + + /// + /// Gets or sets the unique identifier at the specified index. + /// + /// + /// Gets or sets the unique identifier at the specified index. + /// + /// The unique identifier at the specified index. + /// The index. + /// + /// is out of range. + /// + /// + /// The list does not support setting items. + /// + public UniqueId this [int index] { + get { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + uint uid = start <= end ? start + (uint) index : start - (uint) index; + + return new UniqueId (validity, uid); + } + set { + throw new NotSupportedException (); + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the range of unique ids. + /// + /// + /// Gets an enumerator for the range of unique ids. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + if (start <= end) { + for (uint uid = start; uid <= end; uid++) + yield return new UniqueId (validity, uid); + } else { + for (uint uid = start; uid >= end; uid--) + yield return new UniqueId (validity, uid); + } + + yield break; + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the range of unique ids. + /// + /// + /// Gets an enumerator for the range of unique ids. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + if (end == uint.MaxValue) + return string.Format (CultureInfo.InvariantCulture, "{0}:*", start); + + return string.Format (CultureInfo.InvariantCulture, "{0}:{1}", start, end); + } + + /// + /// Attempt to parse a unique identifier range. + /// + /// + /// Attempts to parse a unique identifier range. + /// + /// true if the unique identifier range was successfully parsed; otherwise, false.. + /// The token to parse. + /// The UIDVALIDITY value. + /// The unique identifier range. + /// + /// is null. + /// + public static bool TryParse (string token, uint validity, out UniqueIdRange range) + { + if (token == null) + throw new ArgumentNullException (nameof (token)); + + uint start, end; + int index = 0; + + if (!UniqueId.TryParse (token, ref index, out start) || index + 2 > token.Length || token[index++] != ':') { + range = Invalid; + return false; + } + + if (token[index] != '*') { + if (!UniqueId.TryParse (token, ref index, out end) || index < token.Length) { + range = Invalid; + return false; + } + } else if (index + 1 != token.Length) { + range = Invalid; + return false; + } else { + end = uint.MaxValue; + } + + range = new UniqueIdRange (validity, start, end); + + return true; + } + + /// + /// Attempt to parse a unique identifier range. + /// + /// + /// Attempts to parse a unique identifier range. + /// + /// true if the unique identifier range was successfully parsed; otherwise, false.. + /// The token to parse. + /// The unique identifier range. + /// + /// is null. + /// + public static bool TryParse (string token, out UniqueIdRange range) + { + return TryParse (token, 0, out range); + } + } +} diff --git a/src/MailKit/UniqueIdSet.cs b/src/MailKit/UniqueIdSet.cs new file mode 100644 index 0000000..10228b5 --- /dev/null +++ b/src/MailKit/UniqueIdSet.cs @@ -0,0 +1,988 @@ +// +// UniqueIdSet.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +using MailKit.Search; + +namespace MailKit { + /// + /// A set of unique identifiers. + /// + /// + /// When dealing with a large number of unique identifiers, it may be more efficient to use a + /// than a typical IList<>. + /// + public class UniqueIdSet : IList + { + struct Range + { + public uint Start; + public uint End; + + public Range (uint start, uint end) + { + Start = start; + End = end; + } + + public int Count { + get { return (int) (Start <= End ? End - Start : Start - End) + 1; } + } + + public bool Contains (uint uid) + { + if (Start <= End) + return uid >= Start && uid <= End; + + return uid <= Start && uid >= End; + } + + public int IndexOf (uint uid) + { + if (Start <= End) { + if (uid < Start || uid > End) + return -1; + + return (int) (uid - Start); + } + + if (uid > Start || uid < End) + return -1; + + return (int) (Start - uid); + } + + public uint this [int index] { + get { + return Start <= End ? Start + (uint) index : Start - (uint) index; + } + } + + public IEnumerator GetEnumerator () + { + if (Start <= End) { + for (uint uid = Start; uid <= End; uid++) + yield return uid; + } else { + for (uint uid = Start; uid >= End; uid--) + yield return uid; + } + + yield break; + } + + public override string ToString () + { + if (Start == End) + return Start.ToString (CultureInfo.InvariantCulture); + + if (Start <= End && End == uint.MaxValue) + return string.Format (CultureInfo.InvariantCulture, "{0}:*", Start); + + return string.Format (CultureInfo.InvariantCulture, "{0}:{1}", Start, End); + } + } + + readonly List ranges = new List (); + long count; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new unique identifier set. + /// + /// The uid validity. + /// The sorting order to use for the unique identifiers. + /// + /// is invalid. + /// + public UniqueIdSet (uint validity, SortOrder order = SortOrder.None) + { + switch (order) { + case SortOrder.Descending: + case SortOrder.Ascending: + case SortOrder.None: + break; + default: + throw new ArgumentOutOfRangeException (nameof (order)); + } + + Validity = validity; + SortOrder = order; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new unique identifier set. + /// + /// The sorting order to use for the unique identifiers. + /// + /// is invalid. + /// + public UniqueIdSet (SortOrder order = SortOrder.None) : this (0, order) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new set of unique identifier set containing the specified uids. + /// + /// An initial set of unique ids. + /// The sorting order to use for the unique identifiers. + /// + /// is invalid. + /// + public UniqueIdSet (IEnumerable uids, SortOrder order = SortOrder.None) : this (order) + { + foreach (var uid in uids) + Add (uid); + } + + /// + /// Gets the sort order of the unique identifiers. + /// + /// + /// Gets the sort order of the unique identifiers. + /// + /// The sort order. + public SortOrder SortOrder { + get; private set; + } + + /// + /// Gets the validity, if non-zero. + /// + /// + /// Gets the UidValidity of the containing folder. + /// + /// The UidValidity of the containing folder. + public uint Validity { + get; private set; + } + + #region ICollection implementation + + /// + /// Get the number of unique ids in the set. + /// + /// + /// Gets the number of unique ids in the set. + /// + /// The count. + public int Count { + get { return (int) Math.Min (count, int.MaxValue); } + } + + /// + /// Get whether or not the set is read only. + /// + /// + /// Gets whether or not the set is read-only. + /// + /// true if the set is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + int BinarySearch (uint uid) + { + int min = 0, max = ranges.Count; + + if (max == 0) + return -1; + + do { + int i = min + ((max - min) / 2); + + if (SortOrder == SortOrder.Ascending) { + // sorted ascending: 1:3,5:7,9 + if (uid >= ranges[i].Start) { + if (uid <= ranges[i].End) + return i; + + min = i + 1; + } else { + max = i; + } + } else { + // sorted descending: 9,7:5,3:1 + if (uid >= ranges[i].End) { + if (uid <= ranges[i].Start) + return i; + + max = i; + } else { + min = i + 1; + } + } + } while (min < max); + + return -1; + } + + int IndexOfRange (uint uid) + { + if (SortOrder != SortOrder.None) + return BinarySearch (uid); + + for (int i = 0; i < ranges.Count; i++) { + if (ranges[i].Contains (uid)) + return i; + } + + return -1; + } + + void BinaryInsertAscending (uint uid) + { + int min = 0, max = ranges.Count; + int i; + + do { + i = min + ((max - min) / 2); + + if (uid >= ranges[i].Start) { + if (uid <= ranges[i].End) + return; + + if (uid == ranges[i].End + 1) { + if (i + 1 < ranges.Count && uid + 1 >= ranges[i + 1].Start) { + // merge the 2 ranges together + ranges[i] = new Range (ranges[i].Start, ranges[i + 1].End); + ranges.RemoveAt (i + 1); + count++; + return; + } + + ranges[i] = new Range (ranges[i].Start, uid); + count++; + return; + } + + min = i + 1; + i = min; + } else { + if (uid == ranges[i].Start - 1) { + if (i > 0 && uid - 1 <= ranges[i - 1].End) { + // merge the 2 ranges together + ranges[i - 1] = new Range (ranges[i - 1].Start, ranges[i].End); + ranges.RemoveAt (i); + count++; + return; + } + + ranges[i] = new Range (uid, ranges[i].End); + count++; + return; + } + + max = i; + } + } while (min < max); + + var range = new Range (uid, uid); + + if (i < ranges.Count) + ranges.Insert (i, range); + else + ranges.Add (range); + + count++; + } + + void BinaryInsertDescending (uint uid) + { + int min = 0, max = ranges.Count; + int i; + + do { + i = min + ((max - min) / 2); + + if (uid <= ranges[i].Start) { + if (uid >= ranges[i].End) + return; + + if (uid == ranges[i].End - 1) { + if (i + 1 < ranges.Count && uid - 1 <= ranges[i + 1].Start) { + // merge the 2 ranges together + ranges[i] = new Range (ranges[i].Start, ranges[i + 1].End); + ranges.RemoveAt (i + 1); + count++; + return; + } + + ranges[i] = new Range (ranges[i].Start, uid); + count++; + return; + } + + min = i + 1; + i = min; + } else { + if (uid == ranges[i].Start + 1) { + if (i > 0 && uid + 1 >= ranges[i - 1].End) { + // merge the 2 ranges together + ranges[i - 1] = new Range (ranges[i - 1].Start, ranges[i].End); + ranges.RemoveAt (i); + count++; + return; + } + + ranges[i] = new Range (uid, ranges[i].End); + count++; + return; + } + + max = i; + } + } while (min < max); + + var range = new Range (uid, uid); + + if (i < ranges.Count) + ranges.Insert (i, range); + else + ranges.Add (range); + + count++; + } + + void Append (uint uid) + { + if (IndexOfRange (uid) != -1) + return; + + count++; + + if (ranges.Count > 0) { + int index = ranges.Count - 1; + var range = ranges[index]; + + if (range.Start == range.End) { + if (uid == range.End + 1 || uid == range.End - 1) { + ranges[index] = new Range (range.Start, uid); + return; + } + } else if (range.Start < range.End) { + if (uid == range.End + 1) { + ranges[index] = new Range (range.Start, uid); + return; + } + } else if (range.Start > range.End) { + if (uid == range.End - 1) { + ranges[index] = new Range (range.Start, uid); + return; + } + } + } + + ranges.Add (new Range (uid, uid)); + } + + /// + /// Adds the unique identifier to the set. + /// + /// + /// Adds the unique identifier to the set. + /// + /// The unique identifier to add. + /// + /// is invalid. + /// + public void Add (UniqueId uid) + { + if (!uid.IsValid) + throw new ArgumentException ("Invalid unique identifier.", nameof (uid)); + + if (ranges.Count == 0) { + ranges.Add (new Range (uid.Id, uid.Id)); + count++; + return; + } + + switch (SortOrder) { + case SortOrder.Descending: + BinaryInsertDescending (uid.Id); + break; + case SortOrder.Ascending: + BinaryInsertAscending (uid.Id); + break; + default: + Append (uid.Id); + break; + } + } + + /// + /// Adds all of the uids to the set. + /// + /// + /// Adds all of the uids to the set. + /// + /// The collection of uids. + public void AddRange (IEnumerable uids) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + foreach (var uid in uids) + Add (uid); + } + + /// + /// Clears the list. + /// + /// + /// Clears the list. + /// + /// + /// The collection is readonly. + /// + public void Clear () + { + ranges.Clear (); + count = 0; + } + + /// + /// Checks if the set contains the specified unique id. + /// + /// + /// Determines whether or not the set contains the specified unique id. + /// + /// true if the specified unique identifier is in the set; otherwise false. + /// The unique id. + public bool Contains (UniqueId uid) + { + return IndexOfRange (uid.Id) != -1; + } + + /// + /// Copies all of the unique ids in the set to the specified array. + /// + /// + /// Copies all of the unique ids within the set into the array, + /// starting at the specified array index. + /// + /// The array to copy the unique ids to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of set. + /// + public void CopyTo (UniqueId[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex > (array.Length - Count)) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + int index = arrayIndex; + + for (int i = 0; i < ranges.Count; i++) { + foreach (var uid in ranges[i]) + array[index++] = new UniqueId (Validity, uid); + } + } + + void Remove (int index, uint uid) + { + var range = ranges[index]; + + if (uid == range.Start) { + // remove the first item in the range + if (range.Start != range.End) { + if (range.Start <= range.End) + ranges[index] = new Range (uid + 1, range.End); + else + ranges[index] = new Range (uid - 1, range.End); + } else { + ranges.RemoveAt (index); + } + } else if (uid == range.End) { + // remove the last item in the range + if (range.Start <= range.End) + ranges[index] = new Range (range.Start, uid - 1); + else + ranges[index] = new Range (range.Start, uid + 1); + } else { + // remove a uid from the middle of the range + if (range.Start < range.End) { + ranges.Insert (index, new Range (range.Start, uid - 1)); + ranges[index + 1] = new Range (uid + 1, range.End); + } else { + ranges.Insert (index, new Range (range.Start, uid + 1)); + ranges[index + 1] = new Range (uid - 1, range.End); + } + } + + count--; + } + + /// + /// Removes the unique identifier from the set. + /// + /// + /// Removes the unique identifier from the set. + /// + /// true if the unique identifier was removed; otherwise false. + /// The unique identifier to remove. + public bool Remove (UniqueId uid) + { + int index = IndexOfRange (uid.Id); + + if (index == -1) + return false; + + Remove (index, uid.Id); + + return true; + } + + #endregion + + #region IList implementation + + /// + /// Gets the index of the specified unique id, if it exists. + /// + /// + /// Finds the index of the specified unique id, if it exists. + /// + /// The index of the specified unique id; otherwise -1. + /// The unique id. + public int IndexOf (UniqueId uid) + { + int index = 0; + + for (int i = 0; i < ranges.Count; i++) { + if (ranges[i].Contains (uid.Id)) + return index + ranges[i].IndexOf (uid.Id); + + index += ranges[i].Count; + } + + return -1; + } + + /// + /// Inserts the specified unique identifier at the given index. + /// + /// + /// Inserts the unique identifier at the specified index in the set. + /// + /// The index to insert the unique id. + /// The unique id. + /// + /// The list does not support inserting items. + /// + public void Insert (int index, UniqueId uid) + { + throw new NotSupportedException (); + } + + /// + /// Removes the unique identifier at the specified index. + /// + /// + /// Removes the unique identifier at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index >= count) + throw new ArgumentOutOfRangeException (nameof (index)); + + int offset = 0; + + for (int i = 0; i < ranges.Count; i++) { + if (index >= offset + ranges[i].Count) { + offset += ranges[i].Count; + continue; + } + + var uid = ranges[i][index - offset]; + Remove (i, uid); + return; + } + } + + /// + /// Gets or sets the unique identifier at the specified index. + /// + /// + /// Gets or sets the unique identifier at the specified index. + /// + /// The unique identifier at the specified index. + /// The index. + /// + /// is out of range. + /// + /// + /// The list does not support setting items. + /// + public UniqueId this [int index] { + get { + if (index < 0 || index >= count) + throw new ArgumentOutOfRangeException (nameof (index)); + + int offset = 0; + + for (int i = 0; i < ranges.Count; i++) { + if (index >= offset + ranges[i].Count) { + offset += ranges[i].Count; + continue; + } + + uint uid = ranges[i][index - offset]; + + return new UniqueId (Validity, uid); + } + + throw new ArgumentOutOfRangeException (nameof (index)); + } + set { + throw new NotSupportedException (); + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the set of unique ids. + /// + /// + /// Gets an enumerator for the set of unique ids. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + for (int i = 0; i < ranges.Count; i++) { + foreach (var uid in ranges[i]) + yield return new UniqueId (Validity, uid); + } + + yield break; + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the set of unique ids. + /// + /// + /// Gets an enumerator for the set of unique ids. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + foreach (var subset in EnumerateSerializedSubsets (int.MaxValue)) + return subset; + + return string.Empty; + } + + /// + /// Format a generic list of unique identifiers as a string. + /// + /// + /// Formats a generic list of unique identifiers as a string. + /// + /// The string representation of the collection of unique identifiers. + /// The unique identifiers. + /// + /// is null. + /// + /// + /// One or more of the unique identifiers is invalid (has a value of 0). + /// + public static string ToString (IList uids) + { + foreach (var subset in EnumerateSerializedSubsets (uids, int.MaxValue)) + return subset; + + return string.Empty; + } + + /// + /// Format the set of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// + /// Formats the set of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// A list of strings representing the collection of unique identifiers. + /// The maximum length of any returned string of UIDs. + /// + /// is negative. + /// + IEnumerable EnumerateSerializedSubsets (int maxLength) + { + if (maxLength < 0) + throw new ArgumentOutOfRangeException (nameof (maxLength)); + + var builder = new StringBuilder (); + + for (int i = 0; i < ranges.Count; i++) { + var range = ranges[i].ToString (); + + if (builder.Length > 0) { + if (builder.Length + 1 + range.Length > maxLength) { + yield return builder.ToString (); + builder.Clear (); + } else { + builder.Append (','); + } + } + + builder.Append (range); + } + + yield return builder.ToString (); + } + + /// + /// Format a generic list of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// + /// Formats a generic list of unique identifiers as multiple strings that fit within the maximum defined character length. + /// + /// A list of strings representing the collection of unique identifiers. + /// The unique identifiers. + /// The maximum length of any returned string of UIDs. + /// + /// is null. + /// + /// + /// One or more of the unique identifiers is invalid (has a value of 0). + /// + /// + /// is negative. + /// + internal static IEnumerable EnumerateSerializedSubsets (IList uids, int maxLength) + { + if (uids == null) + throw new ArgumentNullException (nameof (uids)); + + if (maxLength < 0) + throw new ArgumentOutOfRangeException (nameof (maxLength)); + + if (uids.Count == 0) { + yield return string.Empty; + yield break; + } + + var range = uids as UniqueIdRange; + if (range != null) { + yield return range.ToString (); + yield break; + } + + var set = uids as UniqueIdSet; + if (set != null) { + foreach (var subset in set.EnumerateSerializedSubsets (maxLength)) + yield return subset; + yield break; + } + + var builder = new StringBuilder (); + int index = 0; + + while (index < uids.Count) { + if (!uids[index].IsValid) + throw new ArgumentException ("One or more of the uids is invalid.", nameof (uids)); + + uint start = uids[index].Id; + uint end = uids[index].Id; + int i = index + 1; + + if (i < uids.Count) { + if (uids[i].Id == end + 1) { + end = uids[i++].Id; + + while (i < uids.Count && uids[i].Id == end + 1) { + end++; + i++; + } + } else if (uids[i].Id == end - 1) { + end = uids[i++].Id; + + while (i < uids.Count && uids[i].Id == end - 1) { + end--; + i++; + } + } + } + + string next; + if (start != end) + next = string.Format (CultureInfo.InvariantCulture, "{0}:{1}", start, end); + else + next = start.ToString (); + + if (builder.Length > 0) { + if (builder.Length + 1 + next.Length > maxLength) { + yield return builder.ToString (); + builder.Clear (); + } else { + builder.Append (','); + } + } + + builder.Append (next); + index = i; + } + + yield return builder.ToString (); + } + + /// + /// Attempt to parse the specified token as a set of unique identifiers. + /// + /// + /// Attempts to parse the specified token as a set of unique identifiers. + /// + /// true if the set of unique identifiers were successfully parsed; otherwise, false. + /// The token containing the set of unique identifiers. + /// The UIDVALIDITY value. + /// The set of unique identifiers. + /// + /// is null. + /// + public static bool TryParse (string token, uint validity, out UniqueIdSet uids) + { + if (token == null) + throw new ArgumentNullException (nameof (token)); + + uids = new UniqueIdSet (validity); + + var order = SortOrder.None; + bool sorted = true; + uint start, end; + uint prev = 0; + int index = 0; + + do { + if (!UniqueId.TryParse (token, ref index, out start)) + return false; + + if (index < token.Length && token[index] == ':') { + index++; + + if (!UniqueId.TryParse (token, ref index, out end)) + return false; + + var range = new Range (start, end); + uids.count += range.Count; + uids.ranges.Add (range); + + if (sorted) { + switch (order) { + default: sorted = true; order = start <= end ? SortOrder.Ascending : SortOrder.Descending; break; + case SortOrder.Descending: sorted = start >= end && start <= prev; break; + case SortOrder.Ascending: sorted = start <= end && start >= prev; break; + } + } + + prev = end; + } else { + uids.ranges.Add (new Range (start, start)); + uids.count++; + + if (sorted && uids.ranges.Count > 1) { + switch (order) { + default: sorted = true; order = start >= prev ? SortOrder.Ascending : SortOrder.Descending; break; + case SortOrder.Descending: sorted = start <= prev; break; + case SortOrder.Ascending: sorted = start >= prev; break; + } + } + + prev = start; + } + + if (index >= token.Length) + break; + + if (token[index++] != ',') + return false; + } while (true); + + uids.SortOrder = sorted ? order : SortOrder.None; + + return true; + } + + /// + /// Attempt to parse the specified token as a set of unique identifiers. + /// + /// + /// Attempts to parse the specified token as a set of unique identifiers. + /// + /// true if the set of unique identifiers were successfully parsed; otherwise, false. + /// The token containing the set of unique identifiers. + /// The set of unique identifiers. + /// + /// is null. + /// + public static bool TryParse (string token, out UniqueIdSet uids) + { + return TryParse (token, 0, out uids); + } + } +} diff --git a/src/MailKit/UriExtensions.cs b/src/MailKit/UriExtensions.cs new file mode 100644 index 0000000..326ae2f --- /dev/null +++ b/src/MailKit/UriExtensions.cs @@ -0,0 +1,70 @@ +// +// UriExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MailKit { + static class UriExtensions + { + public static IDictionary ParsedQuery (this Uri uri) + { + var properties = new Dictionary (StringComparer.OrdinalIgnoreCase); + int index = 1; + + if (string.IsNullOrEmpty (uri.Query)) + return properties; + + // Note: the query string begins with '?' + while (index < uri.Query.Length) { + int startIndex = index; + + while (index < uri.Query.Length && uri.Query[index] != '=') + index++; + + var name = uri.Query.Substring (startIndex, index - startIndex); + + if (index >= uri.Query.Length) { + properties.Add (name, string.Empty); + break; + } + + startIndex = ++index; + + while (index < uri.Query.Length && uri.Query[index] != '&') + index++; + + var value = uri.Query.Substring (startIndex, index - startIndex); + + properties.Add (name, Uri.UnescapeDataString (value)); + + index++; + } + + return properties; + } + } +} diff --git a/src/MailKit/mailkit.snk b/src/MailKit/mailkit.snk new file mode 100644 index 0000000000000000000000000000000000000000..31cbc70681e54cada3fd1fdddf94c98762c9d53a GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONaL0002Z;t6vs<_eFe4(3D4GU>m8NwyFe#j$2c zn~GenMDh5Hsd~BD;59-SGp6v>R;q6t1LEBHW;??VwyxSKhd#hUTc+$^6A+w~B1>7;_s3CR&^;l)%u=>MBB zD(z7hH_PN;$;TPSJ#$7N^^9CQU^~1#1=0L3h`L=3!r~71p)NQdlgIK3b1`Pf@G12@ znM8Cthy|ZT9e%}KzZYXPhYElsv*mn@d;V9@-_9|}zHH~5x;Rbtb-~yQFmdW7KNzd4 zr|;se3DP-wgCj#`GU*jCN6pn^^ft=T7$PBeFC7j@NjV-1*-{H4f$6U1gSl?l#!V{? zLG=kWfurgtLGA*$gOgI|e}<(wB$4Wz(x8AqZZHEl5iE3VgZ;Z3&q2I=1mgwp$dxWo zNipqOzh+#XHA|AOdUYpn8EMDJd$M@(g(P^zs^@9-pXGg!`~r0uk4}&22o7uC;f={> zRur>a-G1CFpA{B4DDN=>=;~-g0=+b~)!(MQo=DQS1&-Vp>W~b(OO`-Hsr+?$Evv|0 z!XWLKe$Z_3uP~L5g(Y}$vg;p4-P==0Qn(PM-WBr^$So^==5V&1pJq#x!?*X`ML$p2 zd&1wt#oN(xm{9Y?8~kkIZBJ$en{aupu$Z*83XsMTA%q$aWmByS7*exK@p8nzPKstW ipdz8<^dUhz_G*8hn!L|54jMjIa21sQfL_B_4IXZ>A|FBk literal 0 HcmV?d00001 diff --git a/src/MailKitMailModule.cs b/src/MailKitMailModule.cs new file mode 100644 index 0000000..aaeb1f3 --- /dev/null +++ b/src/MailKitMailModule.cs @@ -0,0 +1,14 @@ +using MimeKit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenSim.Modules.EMail +{ + class MailKitMailModule + { + + } +} diff --git a/src/MimeKit/AsyncMimeParser.cs b/src/MimeKit/AsyncMimeParser.cs new file mode 100644 index 0000000..3471a50 --- /dev/null +++ b/src/MimeKit/AsyncMimeParser.cs @@ -0,0 +1,706 @@ +// +// AsyncMimeParser.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit { + public partial class MimeParser + { + async Task ReadAheadAsync (int atleast, int save, CancellationToken cancellationToken) + { + int left, start, end; + + if (!AlignReadAheadBuffer (atleast, save, out left, out start, out end)) + return left; + + int nread = await stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false); + + if (nread > 0) { + inputEnd += nread; + position += nread; + } else { + eos = true; + } + + return inputEnd - inputIndex; + } + + async Task StepByteOrderMarkAsync (CancellationToken cancellationToken) + { + int bomIndex = 0; + + do { + var available = await ReadAheadAsync (ReadAheadSize, 0, cancellationToken).ConfigureAwait (false); + + if (available <= 0) { + // failed to read any data... EOF + inputIndex = inputEnd; + return false; + } + + unsafe { + fixed (byte* inbuf = input) { + StepByteOrderMark (inbuf, ref bomIndex); + } + } + } while (inputIndex == inputEnd); + + return bomIndex == 0 || bomIndex == UTF8ByteOrderMark.Length; + } + + async Task StepMboxMarkerAsync (CancellationToken cancellationToken) + { + bool complete = false; + bool needInput; + int left = 0; + + mboxMarkerLength = 0; + + do { + var available = await ReadAheadAsync (Math.Max (ReadAheadSize, left), 0, cancellationToken).ConfigureAwait (false); + + if (available <= left) { + // failed to find a From line; EOF reached + state = MimeParserState.Error; + inputIndex = inputEnd; + return; + } + + needInput = false; + + unsafe { + fixed (byte* inbuf = input) { + StepMboxMarker (inbuf, ref needInput, ref complete, ref left); + } + } + } while (!complete); + + state = MimeParserState.MessageHeaders; + } + + async Task StepHeadersAsync (CancellationToken cancellationToken) + { + bool scanningFieldName = true; + bool checkFolded = false; + bool midline = false; + bool blank = false; + bool valid = true; + int left = 0; + + headerBlockBegin = GetOffset (inputIndex); + boundary = BoundaryType.None; + ResetRawHeaderData (); + headers.Clear (); + + await ReadAheadAsync (Math.Max (ReadAheadSize, left), 0, cancellationToken).ConfigureAwait (false); + + do { + unsafe { + fixed (byte *inbuf = input) { + if (!StepHeaders (inbuf, ref scanningFieldName, ref checkFolded, ref midline, ref blank, ref valid, ref left)) + break; + } + } + + var available = await ReadAheadAsync (left + 1, 0, cancellationToken).ConfigureAwait (false); + + if (available == left) { + // EOF reached before we reached the end of the headers... + if (scanningFieldName && left > 0) { + // EOF reached right in the middle of a header field name. Throw an error. + // + // See private email from Feb 8, 2018 which contained a sample message w/o + // any breaks between the header and message body. The file also did not + // end with a newline sequence. + state = MimeParserState.Error; + } else { + // EOF reached somewhere in the middle of the value. + // + // Append whatever data we've got left and pretend we found the end + // of the header value (and the header block). + // + // For more details, see https://github.com/jstedfast/MimeKit/pull/51 + // and https://github.com/jstedfast/MimeKit/issues/348 + if (left > 0) { + AppendRawHeaderData (inputIndex, left); + inputIndex = inputEnd; + } + + ParseAndAppendHeader (); + + state = MimeParserState.Content; + } + break; + } + } while (true); + + headerBlockEnd = GetOffset (inputIndex); + } + + async Task SkipLineAsync (bool consumeNewLine, CancellationToken cancellationToken) + { + do { + unsafe { + fixed (byte* inbuf = input) { + if (SkipLine (inbuf, consumeNewLine)) + return true; + } + } + + if (await ReadAheadAsync (ReadAheadSize, 1, cancellationToken).ConfigureAwait (false) <= 0) + return false; + } while (true); + } + + async Task StepAsync (CancellationToken cancellationToken) + { + switch (state) { + case MimeParserState.Initialized: + if (!await StepByteOrderMarkAsync (cancellationToken).ConfigureAwait (false)) { + state = MimeParserState.Eos; + break; + } + + state = format == MimeFormat.Mbox ? MimeParserState.MboxMarker : MimeParserState.MessageHeaders; + break; + case MimeParserState.MboxMarker: + await StepMboxMarkerAsync (cancellationToken).ConfigureAwait (false); + break; + case MimeParserState.MessageHeaders: + case MimeParserState.Headers: + await StepHeadersAsync (cancellationToken).ConfigureAwait (false); + toplevel = false; + break; + } + + return state; + } + + async Task ScanContentAsync (Stream content, bool trimNewLine, CancellationToken cancellationToken) + { + int atleast = Math.Max (ReadAheadSize, GetMaxBoundaryLength ()); + int contentIndex = inputIndex; + var formats = new bool[2]; + bool midline = false; + int nleft; + + do { + if (contentIndex < inputIndex) + content.Write (input, contentIndex, inputIndex - contentIndex); + + nleft = inputEnd - inputIndex; + if (await ReadAheadAsync (atleast, 2, cancellationToken).ConfigureAwait (false) <= 0) { + boundary = BoundaryType.Eos; + contentIndex = inputIndex; + break; + } + + unsafe { + fixed (byte* inbuf = input) { + ScanContent (inbuf, ref contentIndex, ref nleft, ref midline, ref formats); + } + } + } while (boundary == BoundaryType.None); + + if (contentIndex < inputIndex) + content.Write (input, contentIndex, inputIndex - contentIndex); + + var isEmpty = content.Length == 0; + + if (boundary != BoundaryType.Eos && trimNewLine) { + // the last \r\n belongs to the boundary + if (content.Length > 0) { + if (input[inputIndex - 2] == (byte) '\r') + content.SetLength (content.Length - 2); + else + content.SetLength (content.Length - 1); + } + } + + return new ScanContentResult (formats, isEmpty); + } + + async Task ConstructMimePartAsync (MimePart part, CancellationToken cancellationToken) + { + long endOffset, beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + ScanContentResult result; + Stream content; + + OnMimeContentBegin (part, beginOffset); + + if (persistent) { + using (var measured = new MeasuringStream ()) { + result = await ScanContentAsync (measured, true, cancellationToken).ConfigureAwait (false); + endOffset = beginOffset + measured.Length; + } + + content = new BoundStream (stream, beginOffset, endOffset, true); + } else { + content = new MemoryBlockStream (); + result = await ScanContentAsync (content, true, cancellationToken).ConfigureAwait (false); + content.Seek (0, SeekOrigin.Begin); + endOffset = beginOffset + content.Length; + } + + OnMimeContentEnd (part, endOffset); + OnMimeContentOctets (part, endOffset - beginOffset); + OnMimeContentLines (part, lineNumber - beginLineNumber); + + if (!result.IsEmpty) + part.Content = new MimeContent (content, part.ContentTransferEncoding) { NewLineFormat = result.Format }; + else + content.Dispose (); + } + + async Task ConstructMessagePartAsync (MessagePart rfc822, int depth, CancellationToken cancellationToken) + { + var beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + + OnMimeContentBegin (rfc822, beginOffset); + + if (bounds.Count > 0) { + int atleast = Math.Max (ReadAheadSize, GetMaxBoundaryLength ()); + + if (await ReadAheadAsync (atleast, 0, cancellationToken).ConfigureAwait (false) <= 0) { + boundary = BoundaryType.Eos; + return; + } + + unsafe { + fixed (byte* inbuf = input) { + byte* start = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + byte* inptr = start; + + *inend = (byte) '\n'; + + while (*inptr != (byte) '\n') + inptr++; + + boundary = CheckBoundary (inputIndex, start, (int) (inptr - start)); + + switch (boundary) { + case BoundaryType.ImmediateEndBoundary: + case BoundaryType.ImmediateBoundary: + case BoundaryType.ParentBoundary: + return; + case BoundaryType.ParentEndBoundary: + // ignore "From " boundaries, broken mailers tend to include these... + if (!IsMboxMarker (start)) + return; + break; + } + } + } + } + + // parse the headers... + state = MimeParserState.MessageHeaders; + if (await StepAsync (cancellationToken).ConfigureAwait (false) == MimeParserState.Error) { + // Note: this either means that StepHeaders() found the end of the stream + // or an invalid header field name at the start of the message headers, + // which likely means that this is not a valid MIME stream? + boundary = BoundaryType.Eos; + return; + } + + var message = new MimeMessage (options, headers, RfcComplianceMode.Loose); + var type = GetContentType (null); + + if (preHeaderBuffer.Length > 0) { + message.MboxMarker = new byte[preHeaderLength]; + Buffer.BlockCopy (preHeaderBuffer, 0, message.MboxMarker, 0, preHeaderLength); + } + + var entity = options.CreateEntity (type, headers, true, depth); + message.Body = entity; + + OnMimeMessageBegin (message, headerBlockBegin); + OnMimeMessageHeadersEnd (message, headerBlockEnd); + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + await ConstructMultipartAsync ((Multipart) entity, depth + 1, cancellationToken).ConfigureAwait (false); + else if (entity is MessagePart) + await ConstructMessagePartAsync ((MessagePart) entity, depth + 1, cancellationToken).ConfigureAwait (false); + else + await ConstructMimePartAsync ((MimePart) entity, cancellationToken).ConfigureAwait (false); + + rfc822.Message = message; + + var endOffset = GetOffset (inputIndex); + OnMimeEntityEnd (entity, endOffset); + OnMimeMessageEnd (message, endOffset); + OnMimeContentEnd (rfc822, endOffset); + OnMimeContentOctets (rfc822, endOffset - beginOffset); + OnMimeContentLines (rfc822, lineNumber - beginLineNumber); + } + + async Task MultipartScanPreambleAsync (Multipart multipart, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + long offset = GetOffset (inputIndex); + + OnMultipartPreambleBegin (multipart, offset); + await ScanContentAsync (memory, false, cancellationToken).ConfigureAwait (false); + multipart.RawPreamble = memory.ToArray (); + OnMultipartPreambleEnd (multipart, offset + memory.Length); + } + } + + async Task MultipartScanEpilogueAsync (Multipart multipart, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + long offset = GetOffset (inputIndex); + + OnMultipartEpilogueBegin (multipart, offset); + var result = await ScanContentAsync (memory, true, cancellationToken).ConfigureAwait (false); + multipart.RawEpilogue = result.IsEmpty ? null : memory.ToArray (); + OnMultipartEpilogueEnd (multipart, offset + memory.Length); + } + } + + async Task MultipartScanSubpartsAsync (Multipart multipart, int depth, CancellationToken cancellationToken) + { + do { + OnMultipartBoundaryBegin (multipart, GetOffset (inputIndex)); + + // skip over the boundary marker + if (!await SkipLineAsync (true, cancellationToken).ConfigureAwait (false)) { + OnMultipartBoundaryEnd (multipart, GetOffset (inputIndex)); + boundary = BoundaryType.Eos; + return; + } + + OnMultipartBoundaryEnd (multipart, GetOffset (inputIndex)); + + // parse the headers + state = MimeParserState.Headers; + if (await StepAsync (cancellationToken).ConfigureAwait (false) == MimeParserState.Error) { + boundary = BoundaryType.Eos; + return; + } + + if (state == MimeParserState.Boundary) { + if (headers.Count == 0) { + if (boundary == BoundaryType.ImmediateBoundary) + continue; + break; + } + + // This part has no content, but that will be handled in ConstructMultipartAsync() + // or ConstructMimePartAsync(). + } + + //if (state == ParserState.Complete && headers.Count == 0) + // return BoundaryType.EndBoundary; + + var type = GetContentType (multipart.ContentType); + var entity = options.CreateEntity (type, headers, false, depth); + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + await ConstructMultipartAsync ((Multipart) entity, depth + 1, cancellationToken).ConfigureAwait (false); + else if (entity is MessagePart) + await ConstructMessagePartAsync ((MessagePart) entity, depth + 1, cancellationToken).ConfigureAwait (false); + else + await ConstructMimePartAsync ((MimePart) entity, cancellationToken).ConfigureAwait (false); + + OnMimeEntityEnd (entity, GetOffset (inputIndex)); + + multipart.Add (entity); + } while (boundary == BoundaryType.ImmediateBoundary); + } + + async Task ConstructMultipartAsync (Multipart multipart, int depth, CancellationToken cancellationToken) + { + var beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + var marker = multipart.Boundary; + long endOffset; + + OnMimeContentBegin (multipart, beginOffset); + + if (marker == null) { +#if DEBUG + Debug.WriteLine ("Multipart without a boundary encountered!"); +#endif + + // Note: this will scan all content into the preamble... + await MultipartScanPreambleAsync (multipart, cancellationToken).ConfigureAwait (false); + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + return; + } + + PushBoundary (marker); + + await MultipartScanPreambleAsync (multipart, cancellationToken).ConfigureAwait (false); + if (boundary == BoundaryType.ImmediateBoundary) + await MultipartScanSubpartsAsync (multipart, depth, cancellationToken).ConfigureAwait (false); + + if (boundary == BoundaryType.ImmediateEndBoundary) { + OnMultipartEndBoundaryBegin (multipart, GetOffset (inputIndex)); + + // consume the end boundary and read the epilogue (if there is one) + multipart.WriteEndBoundary = true; + await SkipLineAsync (false, cancellationToken).ConfigureAwait (false); + PopBoundary (); + + OnMultipartEndBoundaryEnd (multipart, GetOffset (inputIndex)); + + await MultipartScanEpilogueAsync (multipart, cancellationToken).ConfigureAwait (false); + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + return; + } + + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + + multipart.WriteEndBoundary = false; + + // We either found the end of the stream or we found a parent's boundary + PopBoundary (); + + unsafe { + fixed (byte* inbuf = input) { + if (boundary == BoundaryType.ParentEndBoundary && FoundImmediateBoundary (inbuf, true)) + boundary = BoundaryType.ImmediateEndBoundary; + else if (boundary == BoundaryType.ParentBoundary && FoundImmediateBoundary (inbuf, false)) + boundary = BoundaryType.ImmediateBoundary; + } + } + } + + /// + /// Asynchronously parses a list of headers from the stream. + /// + /// + /// Parses a list of headers from the stream. + /// + /// The parsed list of headers. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public async Task ParseHeadersAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + state = MimeParserState.Headers; + if (await StepAsync (cancellationToken).ConfigureAwait (false) == MimeParserState.Error) + throw new FormatException ("Failed to parse headers."); + + state = eos ? MimeParserState.Eos : MimeParserState.Complete; + + var parsed = new HeaderList (options); + foreach (var header in headers) + parsed.Add (header); + + return parsed; + } + + /// + /// Asynchronously parses an entity from the stream. + /// + /// + /// Parses an entity from the stream. + /// + /// The parsed entity. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public async Task ParseEntityAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + // Note: if a previously parsed MimePart's content has been read, + // then the stream position will have moved and will need to be + // reset. + if (persistent && stream.Position != position) + stream.Seek (position, SeekOrigin.Begin); + + state = MimeParserState.Headers; + toplevel = true; + + if (await StepAsync (cancellationToken).ConfigureAwait (false) == MimeParserState.Error) + throw new FormatException ("Failed to parse entity headers."); + + var type = GetContentType (null); + + // Note: we pass 'false' as the 'toplevel' argument here because + // we want the entity to consume all of the headers. + var entity = options.CreateEntity (type, headers, false, 0); + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + await ConstructMultipartAsync ((Multipart) entity, 0, cancellationToken).ConfigureAwait (false); + else if (entity is MessagePart) + await ConstructMessagePartAsync ((MessagePart) entity, 0, cancellationToken).ConfigureAwait (false); + else + await ConstructMimePartAsync ((MimePart) entity, cancellationToken).ConfigureAwait (false); + + OnMimeEntityEnd (entity, GetOffset (inputIndex)); + + if (boundary != BoundaryType.Eos) + state = MimeParserState.Complete; + else + state = MimeParserState.Eos; + + return entity; + } + + /// + /// Asynchronously parses a message from the stream. + /// + /// + /// Parses a message from the stream. + /// + /// The parsed message. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the message. + /// + /// + /// An I/O error occurred. + /// + public async Task ParseMessageAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + // Note: if a previously parsed MimePart's content has been read, + // then the stream position will have moved and will need to be + // reset. + if (persistent && stream.Position != position) + stream.Seek (position, SeekOrigin.Begin); + + // scan the from-line if we are parsing an mbox + while (state != MimeParserState.MessageHeaders) { + switch (await StepAsync (cancellationToken).ConfigureAwait (false)) { + case MimeParserState.Error: + throw new FormatException ("Failed to find mbox From marker."); + case MimeParserState.Eos: + throw new FormatException ("End of stream."); + } + } + + toplevel = true; + + // parse the headers + if (state < MimeParserState.Content && await StepAsync (cancellationToken).ConfigureAwait (false) == MimeParserState.Error) + throw new FormatException ("Failed to parse message headers."); + + var message = new MimeMessage (options, headers, RfcComplianceMode.Loose); + + OnMimeMessageBegin (message, headerBlockBegin); + OnMimeMessageHeadersEnd (message, headerBlockEnd); + + if (format == MimeFormat.Mbox && options.RespectContentLength) { + contentEnd = 0; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i].Id != HeaderId.ContentLength) + continue; + + var value = headers[i].RawValue; + int length, index = 0; + + if (!ParseUtils.SkipWhiteSpace (value, ref index, value.Length)) + continue; + + if (!ParseUtils.TryParseInt32 (value, ref index, value.Length, out length)) + continue; + + contentEnd = GetOffset (inputIndex) + length; + break; + } + } + + var type = GetContentType (null); + var entity = options.CreateEntity (type, headers, true, 0); + message.Body = entity; + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + await ConstructMultipartAsync ((Multipart) entity, 0, cancellationToken).ConfigureAwait (false); + else if (entity is MessagePart) + await ConstructMessagePartAsync ((MessagePart) entity, 0, cancellationToken).ConfigureAwait (false); + else + await ConstructMimePartAsync ((MimePart) entity, cancellationToken).ConfigureAwait (false); + + var endOffset = GetOffset (inputIndex); + OnMimeEntityEnd (entity, endOffset); + OnMimeMessageEnd (message, endOffset); + + if (boundary != BoundaryType.Eos) { + if (format == MimeFormat.Mbox) + state = MimeParserState.MboxMarker; + else + state = MimeParserState.Complete; + } else { + state = MimeParserState.Eos; + } + + return message; + } + } +} diff --git a/src/MimeKit/AttachmentCollection.cs b/src/MimeKit/AttachmentCollection.cs new file mode 100644 index 0000000..d6cea7f --- /dev/null +++ b/src/MimeKit/AttachmentCollection.cs @@ -0,0 +1,653 @@ +// +// AttachmentCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.IO; +using MimeKit.IO.Filters; + +namespace MimeKit { + /// + /// A collection of attachments. + /// + /// + /// The is only used when building a message body with a . + /// + /// + /// + /// + public class AttachmentCollection : IList + { + readonly List attachments; + readonly bool linked; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// If is true, then the attachments + /// are treated as if they are linked to another . + /// + /// If set to true; the attachments are treated as linked resources. + public AttachmentCollection (bool linkedResources) + { + attachments = new List (); + linked = linkedResources; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public AttachmentCollection () : this (false) + { + } + + #region IList implementation + + /// + /// Gets the number of attachments currently in the collection. + /// + /// + /// Indicates the number of attachments in the collection. + /// + /// The number of attachments. + public int Count { + get { return attachments.Count; } + } + + /// + /// Gets whther or not the collection is read-only. + /// + /// + /// A is never read-only. + /// + /// true if the collection is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Gets or sets the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The attachment at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MimeEntity this [int index] { + get { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return attachments[index]; + } + set { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + attachments[index] = value; + } + } + + static void LoadContent (MimePart attachment, Stream stream) + { + var content = new MemoryBlockStream (); + var filter = new BestEncodingFilter (); + var buf = new byte[4096]; + int index, length; + int nread; + + while ((nread = stream.Read (buf, 0, buf.Length)) > 0) { + filter.Filter (buf, 0, nread, out index, out length); + content.Write (buf, 0, nread); + } + + filter.Flush (buf, 0, 0, out index, out length); + content.Position = 0; + + attachment.ContentTransferEncoding = filter.GetBestEncoding (EncodingConstraint.SevenBit); + attachment.Content = new MimeContent (content); + } + + static ContentType GetMimeType (string fileName) + { + var mimeType = MimeTypes.GetMimeType (fileName); + + return ContentType.Parse (mimeType); + } + + MimeEntity CreateAttachment (ContentType contentType, string fileName, Stream stream) + { + MimeEntity attachment; + + if (contentType.IsMimeType ("message", "rfc822")) { + var message = MimeMessage.Load (stream); + var rfc822 = new MessagePart { Message = message }; + + rfc822.ContentDisposition = new ContentDisposition (linked ? ContentDisposition.Inline : ContentDisposition.Attachment); + rfc822.ContentDisposition.FileName = Path.GetFileName (fileName); + rfc822.ContentType.Name = Path.GetFileName (fileName); + + attachment = rfc822; + } else { + MimePart part; + + if (contentType.IsMimeType ("text", "*")) { + // TODO: should we try to auto-detect charsets if no charset parameter is specified? + part = new TextPart (contentType); + } else { + part = new MimePart (contentType); + } + + part.FileName = Path.GetFileName (fileName); + part.IsAttachment = !linked; + + LoadContent (part, stream); + attachment = part; + } + + if (linked) + attachment.ContentLocation = new Uri (Path.GetFileName (fileName), UriKind.Relative); + + return attachment; + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the specified data as an attachment using the supplied Content-Type. + /// The file name parameter is used to set the Content-Location. + /// For a list of known mime-types and their associated file extensions, see + /// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types + /// + /// The newly added attachment . + /// The name of the file. + /// The file data. + /// The mime-type of the file. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// + public MimeEntity Add (string fileName, byte[] data, ContentType contentType) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + if (data == null) + throw new ArgumentNullException (nameof (data)); + + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + using (var stream = new MemoryStream (data, false)) { + var attachment = CreateAttachment (contentType, fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the specified data as an attachment using the supplied Content-Type. + /// The file name parameter is used to set the Content-Location. + /// For a list of known mime-types and their associated file extensions, see + /// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types + /// + /// The newly added attachment . + /// The name of the file. + /// The content stream. + /// The mime-type of the file. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// -or- + /// The stream cannot be read. + /// + public MimeEntity Add (string fileName, Stream stream, ContentType contentType) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (!stream.CanRead) + throw new ArgumentException ("The stream cannot be read.", nameof (stream)); + + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + var attachment = CreateAttachment (contentType, fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the data as an attachment, using the specified file name for deducing + /// the mime-type by extension and for setting the Content-Location. + /// + /// The newly added attachment . + /// The name of the file. + /// The file data to attach. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// + public MimeEntity Add (string fileName, byte[] data) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + if (data == null) + throw new ArgumentNullException (nameof (data)); + + using (var stream = new MemoryStream (data, false)) { + var attachment = CreateAttachment (GetMimeType (fileName), fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the stream as an attachment, using the specified file name for deducing + /// the mime-type by extension and for setting the Content-Location. + /// + /// The newly added attachment . + /// The name of the file. + /// The content stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// -or- + /// The stream cannot be read + /// + public MimeEntity Add (string fileName, Stream stream) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (!stream.CanRead) + throw new ArgumentException ("The stream cannot be read.", nameof (stream)); + + var attachment = CreateAttachment (GetMimeType (fileName), fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the specified file as an attachment using the supplied Content-Type. + /// For a list of known mime-types and their associated file extensions, see + /// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types + /// + /// The newly added attachment . + /// The name of the file. + /// The mime-type of the file. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// + /// + /// The specified file could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + public MimeEntity Add (string fileName, ContentType contentType) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + using (var stream = File.OpenRead (fileName)) { + var attachment = CreateAttachment (contentType, fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the specified file as an attachment. + /// + /// + /// + /// + /// The newly added attachment . + /// The name of the file. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + public MimeEntity Add (string fileName) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The specified file path is empty.", nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) { + var attachment = CreateAttachment (GetMimeType (fileName), fileName, stream); + + attachments.Add (attachment); + + return attachment; + } + } + + /// + /// Add the specified attachment. + /// + /// + /// Adds the specified as an attachment. + /// + /// The attachment. + /// + /// is null. + /// + public void Add (MimeEntity attachment) + { + if (attachment == null) + throw new ArgumentNullException (nameof (attachment)); + + attachments.Add (attachment); + } + + /// + /// Clears the attachment collection. + /// + /// + /// Removes all attachments from the collection. + /// + public void Clear () + { + attachments.Clear (); + } + + /// + /// Checks if the collection contains the specified attachment. + /// + /// + /// Determines whether or not the collection contains the specified attachment. + /// + /// true if the specified attachment exists; + /// otherwise false. + /// The attachment. + /// + /// is null. + /// + public bool Contains (MimeEntity attachment) + { + if (attachment == null) + throw new ArgumentNullException (nameof (attachment)); + + return attachments.Contains (attachment); + } + + /// + /// Copies all of the attachments in the collection to the specified array. + /// + /// + /// Copies all of the attachments within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the attachments to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (MimeEntity[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex >= array.Length) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + attachments.CopyTo (array, arrayIndex); + } + + /// + /// Gets the index of the requested attachment, if it exists. + /// + /// + /// Finds the index of the specified attachment, if it exists. + /// + /// The index of the requested attachment; otherwise -1. + /// The attachment. + /// + /// is null. + /// + public int IndexOf (MimeEntity attachment) + { + if (attachment == null) + throw new ArgumentNullException (nameof (attachment)); + + return attachments.IndexOf (attachment); + } + + /// + /// Inserts the specified attachment at the given index. + /// + /// + /// Inserts the attachment at the specified index. + /// + /// The index to insert the attachment. + /// The attachment. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, MimeEntity attachment) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (attachment == null) + throw new ArgumentNullException (nameof (attachment)); + + attachments.Insert (index, attachment); + } + + /// + /// Removes the specified attachment. + /// + /// + /// Removes the specified attachment. + /// + /// true if the attachment was removed; otherwise false. + /// The attachment. + /// + /// is null. + /// + public bool Remove (MimeEntity attachment) + { + if (attachment == null) + throw new ArgumentNullException (nameof (attachment)); + + return attachments.Remove (attachment); + } + + /// + /// Removes the attachment at the specified index. + /// + /// + /// Removes the attachment at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + attachments.RemoveAt (index); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of attachments. + /// + /// + /// Gets an enumerator for the list of attachments. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return attachments.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of attachments. + /// + /// + /// Gets an enumerator for the list of attachments. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/src/MimeKit/BodyBuilder.cs b/src/MimeKit/BodyBuilder.cs new file mode 100644 index 0000000..8aae0ef --- /dev/null +++ b/src/MimeKit/BodyBuilder.cs @@ -0,0 +1,186 @@ +// +// BodyBuilder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A message body builder. + /// + /// + /// is a helper class for building common MIME body structures. + /// + /// + /// + /// + public class BodyBuilder + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + public BodyBuilder () + { + LinkedResources = new AttachmentCollection (true); + Attachments = new AttachmentCollection (); + } + + /// + /// Get the attachments. + /// + /// + /// Represents a collection of file attachments that will be included in the message. + /// + /// + /// + /// + /// The attachments. + public AttachmentCollection Attachments { + get; private set; + } + + /// + /// Get the linked resources. + /// + /// + /// Linked resources are a special type of attachment which are linked to from the . + /// + /// + /// + /// + /// The linked resources. + public AttachmentCollection LinkedResources { + get; private set; + } + + /// + /// Get or set the text body. + /// + /// + /// Represents the plain-text formatted version of the message body. + /// + /// + /// + /// + /// The text body. + public string TextBody { + get; set; + } + + /// + /// Get or set the html body. + /// + /// + /// Represents the html formatted version of the message body and may link to any of the . + /// + /// + /// + /// + /// The html body. + public string HtmlBody { + get; set; + } + + /// + /// Construct the message body based on the text-based bodies, the linked resources, and the attachments. + /// + /// + /// Combines the , , , + /// and into the proper MIME structure suitable for display in many common + /// mail clients. + /// + /// + /// + /// + /// The message body. + public MimeEntity ToMessageBody () + { + MultipartAlternative alternative = null; + MimeEntity body = null; + + if (TextBody != null) { + var text = new TextPart ("plain"); + text.Text = TextBody; + + if (HtmlBody != null) { + alternative = new MultipartAlternative (); + alternative.Add (text); + body = alternative; + } else { + body = text; + } + } + + if (HtmlBody != null) { + var text = new TextPart ("html"); + MimeEntity html; + + text.ContentId = MimeUtils.GenerateMessageId (); + text.Text = HtmlBody; + + if (LinkedResources.Count > 0) { + var related = new MultipartRelated { + Root = text + }; + + foreach (var resource in LinkedResources) + related.Add (resource); + + html = related; + } else { + html = text; + } + + if (alternative != null) + alternative.Add (html); + else + body = html; + } + + if (Attachments.Count > 0) { + if (body == null && Attachments.Count == 1) + return Attachments[0]; + + var mixed = new Multipart ("mixed"); + + if (body != null) + mixed.Add (body); + + foreach (var attachment in Attachments) + mixed.Add (attachment); + + body = mixed; + } + + return body ?? new TextPart ("plain") { Text = string.Empty }; + } + } +} diff --git a/src/MimeKit/CancellationToken.cs b/src/MimeKit/CancellationToken.cs new file mode 100644 index 0000000..9b3d8d6 --- /dev/null +++ b/src/MimeKit/CancellationToken.cs @@ -0,0 +1,76 @@ +// +// CancellationToken.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2015 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace System.Threading { + public struct CancellationToken + { + public static readonly CancellationToken None = new CancellationToken (); + + public bool CanBeCancelled { + get { return false; } + } + + public bool IsCancellationRequested { + get { return false; } + } + + public void ThrowIfCancellationRequested () + { + if (IsCancellationRequested) + throw new OperationCanceledException (); + } + + public bool Equals (CancellationToken other) + { + return true; + } + + public override bool Equals (object obj) + { + if (obj is CancellationToken) + return Equals ((CancellationToken) obj); + + return false; + } + + public override int GetHashCode () + { + return base.GetHashCode (); + } + + public static bool operator == (CancellationToken left, CancellationToken right) + { + return left.Equals (right); + } + + public static bool operator != (CancellationToken left, CancellationToken right) + { + return !left.Equals (right); + } + } +} diff --git a/src/MimeKit/ContentDisposition.cs b/src/MimeKit/ContentDisposition.cs new file mode 100644 index 0000000..9912153 --- /dev/null +++ b/src/MimeKit/ContentDisposition.cs @@ -0,0 +1,916 @@ +// +// ContentDisposition.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A class representing a Content-Disposition header value. + /// + /// + /// The Content-Disposition header is a way for the originating client to + /// suggest to the receiving client whether to present the part to the user + /// as an attachment or as part of the content (inline). + /// + public class ContentDisposition + { + /// + /// The attachment disposition. + /// + /// + /// Indicates that the should be treated as an attachment. + /// + public const string Attachment = "attachment"; + + /// + /// The form-data disposition. + /// + /// + /// Indicates that the should be treated as form data. + /// + public const string FormData = "form-data"; + + /// + /// The inline disposition. + /// + /// + /// Indicates that the should be rendered inline. + /// + public const string Inline = "inline"; + + ParameterList parameters; + string disposition; + + /// + /// Initialize a new instance of the class. + /// + /// + /// The disposition should either be + /// or . + /// + /// The disposition. + /// + /// is null. + /// + /// + /// is not "attachment" or "inline". + /// + public ContentDisposition (string disposition) + { + Parameters = new ParameterList (); + Disposition = disposition; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// This is identical to with a disposition + /// value of . + /// + public ContentDisposition () : this (Attachment) + { + } + + static bool IsAsciiAtom (byte c) + { + return c.IsAsciiAtom (); + } + + /// + /// Get or set the disposition. + /// + /// + /// The disposition is typically either "attachment" or "inline". + /// + /// The disposition. + /// + /// is null. + /// + /// + /// is an invalid disposition value. + /// + public string Disposition { + get { return disposition; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0) + throw new ArgumentException ("The disposition is not allowed to be empty.", nameof (value)); + + for (int i = 0; i < value.Length; i++) { + if (value[i] >= 127 || !IsAsciiAtom ((byte) value[i])) + throw new ArgumentException ("Illegal characters in disposition value.", nameof (value)); + } + + if (disposition == value) + return; + + disposition = value; + OnChanged (); + } + } + + /// + /// Get or set a value indicating whether the is an attachment. + /// + /// + /// A convenience property to determine if the entity should be considered an attachment or not. + /// + /// true if the is an attachment; otherwise, false. + public bool IsAttachment { + get { return disposition.Equals (Attachment, StringComparison.OrdinalIgnoreCase); } + set { disposition = value ? Attachment : Inline; } + } + + /// + /// Get the list of parameters on the . + /// + /// + /// In addition to specifying whether the entity should be treated as an + /// attachment vs displayed inline, the Content-Disposition header may also + /// contain parameters to provide further information to the receiving client + /// such as the file attributes. + /// + /// + /// + /// + /// The parameters. + public ParameterList Parameters { + get { return parameters; } + private set { + if (parameters != null) + parameters.Changed -= OnParametersChanged; + + value.Changed += OnParametersChanged; + parameters = value; + } + } + + /// + /// Get or set the name of the file. + /// + /// + /// When set, this can provide a useful hint for a default file name for the + /// content when the user decides to save it to disk. + /// + /// + /// + /// + /// The name of the file. + public string FileName { + get { return Parameters["filename"]; } + set { + if (value != null) + Parameters["filename"] = value; + else + Parameters.Remove ("filename"); + } + } + + static bool IsNullOrWhiteSpace (string value) + { + if (string.IsNullOrEmpty (value)) + return true; + + for (int i = 0; i < value.Length; i++) { + if (!char.IsWhiteSpace (value[i])) + return false; + } + + return true; + } + + /// + /// Get or set the creation-date parameter. + /// + /// + /// Refers to the date and time that the content file was created on the + /// originating system. This parameter serves little purpose and is + /// typically not used by mail clients. + /// + /// The creation date. + public DateTimeOffset? CreationDate { + get { + var value = Parameters["creation-date"]; + if (IsNullOrWhiteSpace (value)) + return null; + + var buffer = Encoding.UTF8.GetBytes (value); + DateTimeOffset ctime; + + if (!DateUtils.TryParse (buffer, 0, buffer.Length, out ctime)) + return null; + + return ctime; + } + set { + if (value.HasValue) + Parameters["creation-date"] = DateUtils.FormatDate (value.Value); + else + Parameters.Remove ("creation-date"); + } + } + + /// + /// Get or set the modification-date parameter. + /// + /// + /// Refers to the date and time that the content file was last modified on + /// the originating system. This parameter serves little purpose and is + /// typically not used by mail clients. + /// + /// The modification date. + public DateTimeOffset? ModificationDate { + get { + var value = Parameters["modification-date"]; + if (IsNullOrWhiteSpace (value)) + return null; + + var buffer = Encoding.UTF8.GetBytes (value); + DateTimeOffset mtime; + + if (!DateUtils.TryParse (buffer, 0, buffer.Length, out mtime)) + return null; + + return mtime; + } + set { + if (value.HasValue) + Parameters["modification-date"] = DateUtils.FormatDate (value.Value); + else + Parameters.Remove ("modification-date"); + } + } + + /// + /// Get or set the read-date parameter. + /// + /// + /// Refers to the date and time that the content file was last read on the + /// originating system. This parameter serves little purpose and is typically + /// not used by mail clients. + /// + /// The read date. + public DateTimeOffset? ReadDate { + get { + var value = Parameters["read-date"]; + if (IsNullOrWhiteSpace (value)) + return null; + + var buffer = Encoding.UTF8.GetBytes (value); + DateTimeOffset atime; + + if (!DateUtils.TryParse (buffer, 0, buffer.Length, out atime)) + return null; + + return atime; + } + set { + if (value.HasValue) + Parameters["read-date"] = DateUtils.FormatDate (value.Value); + else + Parameters.Remove ("read-date"); + } + } + + /// + /// Get or set the size parameter. + /// + /// + /// When set, the size parameter typically refers to the original size of the + /// content on disk. This parameter is rarely used by mail clients as it serves + /// little purpose. + /// + /// The size. + public long? Size { + get { + var value = Parameters["size"]; + if (IsNullOrWhiteSpace (value)) + return null; + + long size; + if (!long.TryParse (value, out size)) + return null; + + return size; + } + set { + if (value.HasValue) + Parameters["size"] = value.Value.ToString (); + else + Parameters.Remove ("size"); + } + } + + internal string Encode (FormatOptions options, Encoding charset) + { + int lineLength = "Content-Disposition:".Length; + var value = new StringBuilder (" "); + + value.Append (disposition); + lineLength += value.Length; + + Parameters.Encode (options, value, ref lineLength, charset); + value.Append (options.NewLine); + + return value.ToString (); + } + + /// + /// Serialize the to a string, + /// optionally encoding the parameters. + /// + /// + /// Creates a string-representation of the , + /// optionally encoding the parameters as they would be encoded for trabsport. + /// + /// The serialized string. + /// The formatting options. + /// The charset to be used when encoding the parameter values. + /// If set to true, the parameter values will be encoded. + /// + /// is null. + /// -or- + /// is null. + /// + public string ToString (FormatOptions options, Encoding charset, bool encode) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + var value = new StringBuilder ("Content-Disposition: "); + value.Append (disposition); + + if (encode) { + int lineLength = value.Length; + + Parameters.Encode (options, value, ref lineLength, charset); + } else { + value.Append (Parameters.ToString ()); + } + + return value.ToString (); + } + + /// + /// Serialize the to a string, + /// optionally encoding the parameters. + /// + /// + /// Creates a string-representation of the , + /// optionally encoding the parameters as they would be encoded for trabsport. + /// + /// The serialized string. + /// The charset to be used when encoding the parameter values. + /// If set to true, the parameter values will be encoded. + /// + /// is null. + /// + public string ToString (Encoding charset, bool encode) + { + return ToString (FormatOptions.Default, charset, encode); + } + + /// + /// Serialize the to a string. + /// + /// + /// Creates a string-representation of the . + /// + /// A that represents the current + /// . + public override string ToString () + { + return ToString (FormatOptions.Default, Encoding.UTF8, false); + } + + internal event EventHandler Changed; + + void OnParametersChanged (object sender, EventArgs e) + { + OnChanged (); + } + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out ContentDisposition disposition) + { + string type; + int atom; + + disposition = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Expected atom token at position {0}", index), index, index); + + return false; + } + + atom = index; + if (text[index] == '"') { + if (throwOnError) + throw new ParseException (string.Format ("Unxpected qstring token at position {0}", atom), atom, index); + + // Note: This is a work-around for broken mailers that quote the disposition value... + // + // See https://github.com/jstedfast/MailKit/issues/486 for details. + if (!ParseUtils.SkipQuoted (text, ref index, endIndex, throwOnError)) + return false; + + type = CharsetUtils.ConvertToUnicode (options, text, atom, index - atom); + type = MimeUtils.Unquote (type); + + if (string.IsNullOrEmpty (type)) + type = Attachment; + } else { + if (!ParseUtils.SkipAtom (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid atom token at position {0}", atom), atom, index); + + // Note: this is a work-around for broken mailers that do not specify a disposition value... + // + // See https://github.com/jstedfast/MailKit/issues/486 for details. + if (index > atom || text[index] != (byte) ';') + return false; + + type = Attachment; + } else { + type = Encoding.ASCII.GetString (text, atom, index - atom); + } + } + + disposition = new ContentDisposition (); + disposition.disposition = type; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + return true; + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Expected ';' at position {0}", index), index, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + return true; + + ParameterList parameters; + if (!ParameterList.TryParse (options, text, ref index, endIndex, throwOnError, out parameters)) + return false; + + disposition.Parameters = parameters; + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed disposition. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out ContentDisposition disposition) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int index = startIndex; + + return TryParse (options, buffer, ref index, startIndex + length, false, out disposition); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed disposition. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out ContentDisposition disposition) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out disposition); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the specified index. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed disposition. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out ContentDisposition disposition) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int index = startIndex; + + return TryParse (options, buffer, ref index, buffer.Length, false, out disposition); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the specified index. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed disposition. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out ContentDisposition disposition) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out disposition); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the specified buffer. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The parsed disposition. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out ContentDisposition disposition) + { + ParseUtils.ValidateArguments (options, buffer); + + int index = 0; + + return TryParse (options, buffer, ref index, buffer.Length, false, out disposition); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Disposition value from the specified buffer. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The input buffer. + /// The parsed disposition. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out ContentDisposition disposition) + { + return TryParse (ParserOptions.Default, buffer, out disposition); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied text. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The parser options. + /// The text to parse. + /// The parsed disposition. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out ContentDisposition disposition) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + int index = 0; + + return TryParse (ParserOptions.Default, buffer, ref index, buffer.Length, false, out disposition); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a Content-Disposition value from the supplied text. + /// + /// true if the disposition was successfully parsed; otherwise, false. + /// The text to parse. + /// The parsed disposition. + /// + /// is null. + /// + public static bool TryParse (string text, out ContentDisposition disposition) + { + return TryParse (ParserOptions.Default, text, out disposition); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// The start index of the buffer. + /// The length of the buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + ContentDisposition disposition; + int index = startIndex; + + TryParse (options, buffer, ref index, startIndex + length, true, out disposition); + + return disposition; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The input buffer. + /// The start index of the buffer. + /// The length of the buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// The start index of the buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + ContentDisposition disposition; + int index = startIndex; + + TryParse (options, buffer, ref index, buffer.Length, true, out disposition); + + return disposition; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The input buffer. + /// The start index of the buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + ContentDisposition disposition; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, true, out disposition); + + return disposition; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the supplied buffer. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the specified text into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the specified text. + /// + /// The parsed . + /// The parser options. + /// The input text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (ParserOptions options, string text) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + ContentDisposition disposition; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, true, out disposition); + + return disposition; + } + + /// + /// Parse the specified text into a new instance of the class. + /// + /// + /// Parses a Content-Disposition value from the specified text. + /// + /// The parsed . + /// The input text. + /// + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentDisposition Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + } +} diff --git a/src/MimeKit/ContentEncoding.cs b/src/MimeKit/ContentEncoding.cs new file mode 100644 index 0000000..53c6cbd --- /dev/null +++ b/src/MimeKit/ContentEncoding.cs @@ -0,0 +1,83 @@ +// +// ContentEncoding.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// An enumeration of all supported content transfer encodings. + /// + /// + /// Some older mail software is unable to properly deal with + /// data outside of the ASCII range, so it is sometimes + /// necessary to encode the content of MIME entities. + /// + /// + public enum ContentEncoding { + /// + /// The default encoding (aka no encoding at all). + /// + Default, + + /// + /// The 7bit content transfer encoding. This encoding should be restricted to textual content + /// in the US-ASCII range. + /// + SevenBit, + + /// + /// The 8bit content transfer encoding. This encoding should be restricted to textual content + /// outside of the US-ASCII range but may not be supported by all transport services such as + /// older SMTP servers that do not support the 8BITMIME extension. + /// + EightBit, + + /// + /// The binary content transfer encoding. This encoding is simply unencoded binary data. Typically + /// not supported by standard message transport services such as SMTP. + /// + Binary, + + /// + /// The base64 content transfer encoding. This encoding is typically used for encoding binary data + /// or textual content in a largely 8bit charset encoding and is supported by all message transport + /// services. + /// + Base64, + + /// + /// The quoted-printable content transfer encoding. This encoding is used for textual content that + /// is in a charset that has a minority of characters outside of the US-ASCII range (such as + /// ISO-8859-1 and other single-byte charset encodings) and is supported by all message transport + /// services. + /// + QuotedPrintable, + + /// + /// The uuencode content transfer encoding. This is an obsolete encoding meant for encoding binary + /// data and has largely been superceeded by . + /// + UUEncode, + } +} diff --git a/src/MimeKit/ContentType.cs b/src/MimeKit/ContentType.cs new file mode 100644 index 0000000..02244da --- /dev/null +++ b/src/MimeKit/ContentType.cs @@ -0,0 +1,881 @@ +// +// ContentType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A class representing a Content-Type header value. + /// + /// + /// The Content-Type header is a way for the originating client to + /// suggest to the receiving client the mime-type of the content and, + /// depending on that mime-type, presentation options such as charset. + /// + public class ContentType + { + ParameterList parameters; + string type, subtype; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new based on the media type and subtype provided. + /// + /// Media type. + /// Media subtype. + /// + /// is null. + /// -or- + /// is null. + /// + public ContentType (string mediaType, string mediaSubtype) + { + if (mediaType == null) + throw new ArgumentNullException (nameof (mediaType)); + + if (mediaSubtype == null) + throw new ArgumentNullException (nameof (mediaSubtype)); + + Parameters = new ParameterList (); + subtype = mediaSubtype; + type = mediaType; + } + + /// + /// Get or set the type of the media. + /// + /// + /// Represents the media type of the . Examples include + /// "text", "image", and "application". This string should + /// always be treated as case-insensitive. + /// + /// The type of the media. + /// + /// is null. + /// + public string MediaType { + get { return type; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (type == value) + return; + + type = value; + + OnChanged (); + } + } + + /// + /// Get or set the media subtype. + /// + /// + /// Represents the media subtype of the . Examples include + /// "html", "jpeg", and "octet-stream". This string should + /// always be treated as case-insensitive. + /// + /// The media subtype. + /// + /// is null. + /// + public string MediaSubtype { + get { return subtype; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (subtype == value) + return; + + subtype = value; + + OnChanged (); + } + } + + /// + /// Get the list of parameters on the . + /// + /// + /// In addition to the media type and subtype, the Content-Type header may also + /// contain parameters to provide further hints to the receiving client as to + /// how to process or display the content. + /// + /// The parameters. + public ParameterList Parameters { + get { return parameters; } + internal set { + if (parameters != null) + parameters.Changed -= OnParametersChanged; + + value.Changed += OnParametersChanged; + parameters = value; + } + } + + /// + /// Get or set the boundary parameter. + /// + /// + /// This is a special parameter on entities, designating to the + /// parser a unique string that should be considered the boundary marker for each sub-part. + /// + /// The boundary. + public string Boundary { + get { return Parameters["boundary"]; } + set { + if (value != null) + Parameters["boundary"] = value; + else + Parameters.Remove ("boundary"); + } + } + + /// + /// Get or set the charset parameter. + /// + /// + /// Text-based entities will often include a charset parameter + /// so that the receiving client can properly render the text. + /// + /// The charset. + public string Charset { + get { return Parameters["charset"]; } + set { + if (value != null) + Parameters["charset"] = value; + else + Parameters.Remove ("charset"); + } + } + + /// + /// Get or set the charset parameter as an . + /// + /// + /// Text-based entities will often include a charset parameter + /// so that the receiving client can properly render the text. + /// + /// The charset encoding. + public Encoding CharsetEncoding { + get { + var charset = Charset; + + if (charset == null) + return null; + + try { + return CharsetUtils.GetEncoding (charset); + } catch { + return null; + } + } + set { + Charset = value != null ? CharsetUtils.GetMimeCharset (value) : null; + } + } + + /// + /// Get or set the format parameter. + /// + /// + /// The format parameter is typically use with text/plain + /// entities and will either have a value of "fixed" or "flowed". + /// + /// The charset. + public string Format { + get { return Parameters["format"]; } + set { + if (value != null) + Parameters["format"] = value; + else + Parameters.Remove ("format"); + } + } + + /// + /// Gets the simple mime-type. + /// + /// + /// Gets the simple mime-type. + /// + /// The mime-type. + public string MimeType { + get { return string.Format ("{0}/{1}", MediaType, MediaSubtype); } + } + + /// + /// Get or set the name parameter. + /// + /// + /// The name parameter is a way for the originiating client to suggest + /// to the receiving client a display-name for the content, which may + /// be used by the receiving client if it cannot display the actual + /// content to the user. + /// + /// The name. + public string Name { + get { return Parameters["name"]; } + set { + if (value != null) + Parameters["name"] = value; + else + Parameters.Remove ("name"); + } + } + + /// + /// Check if the this instance of matches + /// the specified MIME media type and subtype. + /// + /// + /// If the specified or + /// are "*", they match anything. + /// + /// true if the matches the + /// provided media type and subtype. + /// The media type. + /// The media subtype. + /// + /// is null. + /// -or- + /// is null. + /// + public bool IsMimeType (string mediaType, string mediaSubtype) + { + if (mediaType == null) + throw new ArgumentNullException (nameof (mediaType)); + + if (mediaSubtype == null) + throw new ArgumentNullException (nameof (mediaSubtype)); + + if (mediaType == "*" || mediaType.Equals (type, StringComparison.OrdinalIgnoreCase)) + return mediaSubtype == "*" || mediaSubtype.Equals (subtype, StringComparison.OrdinalIgnoreCase); + + return false; + } + + internal string Encode (FormatOptions options, Encoding charset) + { + int lineLength = "Content-Type:".Length; + var value = new StringBuilder (" "); + + value.Append (MediaType); + value.Append ('/'); + value.Append (MediaSubtype); + + lineLength += value.Length; + + Parameters.Encode (options, value, ref lineLength, charset); + value.Append (options.NewLine); + + return value.ToString (); + } + + /// + /// Serialize the to a string, + /// optionally encoding the parameters. + /// + /// + /// Creates a string-representation of the , optionally encoding + /// the parameters as they would be encoded for transport. + /// + /// The serialized string. + /// The formatting options. + /// The charset to be used when encoding the parameter values. + /// If set to true, the parameter values will be encoded. + /// + /// is null. + /// -or- + /// is null. + /// + public string ToString (FormatOptions options, Encoding charset, bool encode) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + var value = new StringBuilder ("Content-Type: "); + value.Append (MediaType); + value.Append ('/'); + value.Append (MediaSubtype); + + if (encode) { + int lineLength = value.Length; + + Parameters.Encode (options, value, ref lineLength, charset); + } else { + value.Append (Parameters.ToString ()); + } + + return value.ToString (); + } + + /// + /// Serialize the to a string, + /// optionally encoding the parameters. + /// + /// + /// Creates a string-representation of the , optionally encoding + /// the parameters as they would be encoded for transport. + /// + /// The serialized string. + /// The charset to be used when encoding the parameter values. + /// If set to true, the parameter values will be encoded. + /// + /// is null. + /// + public string ToString (Encoding charset, bool encode) + { + return ToString (FormatOptions.Default, charset, encode); + } + + /// + /// Serialize the to a string. + /// + /// + /// Creates a string-representation of the . + /// + /// A that represents the current + /// . + public override string ToString () + { + return ToString (FormatOptions.Default, Encoding.UTF8, false); + } + + internal event EventHandler Changed; + + void OnParametersChanged (object sender, EventArgs e) + { + OnChanged (); + } + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + static bool SkipType (byte[] text, ref int index, int endIndex) + { + int startIndex = index; + + while (index < endIndex && text[index].IsAsciiAtom () && text[index] != (byte) '/') + index++; + + return index > startIndex; + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out ContentType contentType) + { + string type, subtype; + int start; + + contentType = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + start = index; + if (!SkipType (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid type token at position {0}", start), start, index); + + return false; + } + + type = Encoding.ASCII.GetString (text, start, index - start); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || text[index] != (byte) '/') { + if (throwOnError) + throw new ParseException (string.Format ("Expected '/' at position {0}", index), index, index); + + return false; + } + + // skip over the '/' + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + start = index; + if (!ParseUtils.SkipToken (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid atom token at position {0}", start), start, index); + + return false; + } + + subtype = Encoding.ASCII.GetString (text, start, index - start); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + contentType = new ContentType (type, subtype); + + if (index >= endIndex) + return true; + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Expected ';' at position {0}", index), index, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + return true; + + ParameterList parameters; + if (!ParameterList.TryParse (options, text, ref index, endIndex, throwOnError, out parameters)) + return false; + + contentType.Parameters = parameters; + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed content type. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out ContentType type) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int index = startIndex; + + return TryParse (options, buffer, ref index, startIndex + length, false, out type); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed content type. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out ContentType type) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out type); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the specified index. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed content type. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out ContentType type) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int index = startIndex; + + return TryParse (options, buffer, ref index, buffer.Length, false, out type); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the specified index. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed content type. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out ContentType type) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out type); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the specified buffer. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The parser options. + /// The input buffer. + /// The parsed content type. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out ContentType type) + { + ParseUtils.ValidateArguments (options, buffer); + + int index = 0; + + return TryParse (options, buffer, ref index, buffer.Length, false, out type); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a Content-Type value from the specified buffer. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The input buffer. + /// The parsed content type. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out ContentType type) + { + return TryParse (ParserOptions.Default, buffer, out type); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a Content-Type value from the specified text. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// THe parser options. + /// The text to parse. + /// The parsed content type. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out ContentType type) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + int index = 0; + + return TryParse (options, buffer, ref index, buffer.Length, false, out type); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a Content-Type value from the specified text. + /// + /// true if the content type was successfully parsed; otherwise, false. + /// The text to parse. + /// The parsed content type. + /// + /// is null. + /// + public static bool TryParse (string text, out ContentType type) + { + return TryParse (ParserOptions.Default, text, out type); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// The start index of the buffer. + /// The length of the buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int index = startIndex; + ContentType type; + + TryParse (options, buffer, ref index, startIndex + length, true, out type); + + return type; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The input buffer. + /// The start index of the buffer. + /// The length of the buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// The start index of the buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int index = startIndex; + ContentType type; + + TryParse (options, buffer, ref index, buffer.Length, true, out type); + + return type; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The input buffer. + /// The start index of the buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the specified buffer. + /// + /// The parsed . + /// The parser options. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + ContentType type; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, true, out type); + + return type; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the specified buffer. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the specified text into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the specified text. + /// + /// The parsed . + /// The parser options. + /// The text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (ParserOptions options, string text) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + ContentType type; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, true, out type); + + return type; + } + + /// + /// Parse the specified text into a new instance of the class. + /// + /// + /// Parses a Content-Type value from the specified text. + /// + /// The parsed . + /// The text. + /// + /// is null. + /// + /// + /// The could not be parsed. + /// + public static ContentType Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + } +} diff --git a/src/MimeKit/Cryptography/ApplicationPgpEncrypted.cs b/src/MimeKit/Cryptography/ApplicationPgpEncrypted.cs new file mode 100644 index 0000000..05859d6 --- /dev/null +++ b/src/MimeKit/Cryptography/ApplicationPgpEncrypted.cs @@ -0,0 +1,97 @@ +// +// ApplicationPgpEncrypted.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; + +namespace MimeKit.Cryptography { + /// + /// A MIME part with a Content-Type of application/pgp-encrypted. + /// + /// + /// An application/pgp-encrypted part will typically be the first child of + /// a part and contains only a Version + /// header. + /// + public class ApplicationPgpEncrypted : MimePart + { + /// + /// Initialize a new instance of the + /// class based on the . + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public ApplicationPgpEncrypted (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new MIME part with a Content-Type of application/pgp-encrypted + /// and content matching "Version: 1\n". + /// + public ApplicationPgpEncrypted () : base ("application", "pgp-encrypted") + { + ContentDisposition = new ContentDisposition ("attachment"); + ContentTransferEncoding = ContentEncoding.SevenBit; + + var content = new MemoryStream (Encoding.UTF8.GetBytes ("Version: 1\n"), false); + + Content = new MimeContent (content); + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitApplicationPgpEncrypted (this); + } + } +} diff --git a/src/MimeKit/Cryptography/ApplicationPgpSignature.cs b/src/MimeKit/Cryptography/ApplicationPgpSignature.cs new file mode 100644 index 0000000..d7590e3 --- /dev/null +++ b/src/MimeKit/Cryptography/ApplicationPgpSignature.cs @@ -0,0 +1,106 @@ +// +// ApplicationPgpSignature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +namespace MimeKit.Cryptography { + /// + /// A MIME part with a Content-Type of application/pgp-signature. + /// + /// + /// An application/pgp-signature part contains detatched pgp signature data + /// and is typically contained within a part. + /// To verify the signature, use the one of the + /// Verify + /// methods on the parent multipart/signed part. + /// + public class ApplicationPgpSignature : MimePart + { + /// + /// Initialize a new instance of the + /// class based on the . + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public ApplicationPgpSignature (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the + /// class with a Content-Type of application/pgp-signature. + /// + /// + /// Creates a new MIME part with a Content-Type of application/pgp-signature + /// and the as its content. + /// + /// The content stream. + /// + /// is null. + /// + /// + /// does not support reading. + /// -or- + /// does not support seeking. + /// + public ApplicationPgpSignature (Stream stream) : base ("application", "pgp-signature") + { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + ContentTransferEncoding = ContentEncoding.SevenBit; + Content = new MimeContent (stream); + FileName = "signature.pgp"; + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitApplicationPgpSignature (this); + } + } +} diff --git a/src/MimeKit/Cryptography/ApplicationPkcs7Mime.cs b/src/MimeKit/Cryptography/ApplicationPkcs7Mime.cs new file mode 100644 index 0000000..12ebd80 --- /dev/null +++ b/src/MimeKit/Cryptography/ApplicationPkcs7Mime.cs @@ -0,0 +1,924 @@ +// +// ApplicationPkcs7Mime.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Collections.Generic; + +using MimeKit.IO; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME part with a Content-Type of application/pkcs7-mime. + /// + /// + /// An application/pkcs7-mime is an S/MIME part and may contain encrypted, + /// signed or compressed data (or any combination of the above). + /// + public class ApplicationPkcs7Mime : MimePart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public ApplicationPkcs7Mime (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new MIME part with a Content-Type of application/pkcs7-mime + /// and the as its content. + /// Unless you are writing your own pkcs7 implementation, you'll probably + /// want to use the , + /// , and/or + /// method to create new instances + /// of this class. + /// + /// The S/MIME type. + /// The content stream. + /// + /// is null. + /// + /// + /// is not a valid value. + /// + /// + /// does not support reading. + /// -or- + /// does not support seeking. + /// + public ApplicationPkcs7Mime (SecureMimeType type, Stream stream) : base ("application", "pkcs7-mime") + { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + ContentTransferEncoding = ContentEncoding.Base64; + Content = new MimeContent (stream); + + switch (type) { + case SecureMimeType.CompressedData: + ContentType.Parameters["smime-type"] = "compressed-data"; + ContentDisposition.FileName = "smime.p7z"; + ContentType.Name = "smime.p7z"; + break; + case SecureMimeType.EnvelopedData: + ContentType.Parameters["smime-type"] = "enveloped-data"; + ContentDisposition.FileName = "smime.p7m"; + ContentType.Name = "smime.p7m"; + break; + case SecureMimeType.SignedData: + ContentType.Parameters["smime-type"] = "signed-data"; + ContentDisposition.FileName = "smime.p7m"; + ContentType.Name = "smime.p7m"; + break; + case SecureMimeType.CertsOnly: + ContentType.Parameters["smime-type"] = "certs-only"; + ContentDisposition.FileName = "smime.p7c"; + ContentType.Name = "smime.p7c"; + break; + default: + throw new ArgumentOutOfRangeException (nameof (type)); + } + } + + /// + /// Gets the value of the "smime-type" parameter. + /// + /// + /// Gets the value of the "smime-type" parameter. + /// + /// The value of the "smime-type" parameter. + public SecureMimeType SecureMimeType { + get { + var type = ContentType.Parameters["smime-type"]; + + if (type == null) + return SecureMimeType.Unknown; + + switch (type.ToLowerInvariant ()) { + case "authenveloped-data": return SecureMimeType.AuthEnvelopedData; + case "compressed-data": return SecureMimeType.CompressedData; + case "enveloped-data": return SecureMimeType.EnvelopedData; + case "signed-data": return SecureMimeType.SignedData; + case "certs-only": return SecureMimeType.CertsOnly; + default: return SecureMimeType.Unknown; + } + } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitApplicationPkcs7Mime (this); + } + + /// + /// Decompress the compressed-data. + /// + /// + /// Decompresses the compressed-data using the specified . + /// + /// The decompressed . + /// The S/MIME context to use for decompressing. + /// + /// is null. + /// + /// + /// The "smime-type" parameter on the Content-Type header is not "compressed-data". + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public MimeEntity Decompress (SecureMimeContext ctx) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (SecureMimeType != SecureMimeType.CompressedData && SecureMimeType != SecureMimeType.Unknown) + throw new InvalidOperationException (); + + using (var memory = new MemoryBlockStream ()) { + Content.DecodeTo (memory); + memory.Position = 0; + + return ctx.Decompress (memory); + } + } + + /// + /// Decompress the compressed-data. + /// + /// + /// Decompresses the compressed-data using the default . + /// + /// The decompressed . + /// + /// The "smime-type" parameter on the Content-Type header is not "compressed-data". + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public MimeEntity Decompress () + { + if (SecureMimeType != SecureMimeType.CompressedData && SecureMimeType != SecureMimeType.Unknown) + throw new InvalidOperationException (); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Decompress (ctx); + } + + /// + /// Decrypt the enveloped-data. + /// + /// + /// Decrypts the enveloped-data using the specified . + /// + /// The decrypted . + /// The S/MIME context to use for decrypting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The "smime-type" parameter on the Content-Type header is not "enveloped-data". + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public MimeEntity Decrypt (SecureMimeContext ctx, CancellationToken cancellationToken = default (CancellationToken)) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (SecureMimeType != SecureMimeType.EnvelopedData && SecureMimeType != SecureMimeType.Unknown) + throw new InvalidOperationException (); + + using (var memory = new MemoryBlockStream ()) { + Content.DecodeTo (memory); + memory.Position = 0; + + return ctx.Decrypt (memory, cancellationToken); + } + } + + /// + /// Decrypt the enveloped-data. + /// + /// + /// Decrypts the enveloped-data using the default . + /// + /// The decrypted . + /// The cancellation token. + /// + /// The "smime-type" parameter on the Content-Type header is not "certs-only". + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public MimeEntity Decrypt (CancellationToken cancellationToken = default (CancellationToken)) + { + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Decrypt (ctx, cancellationToken); + } + + /// + /// Import the certificates contained in the application/pkcs7-mime content. + /// + /// + /// Imports the certificates contained in the application/pkcs7-mime content. + /// + /// The S/MIME context to import certificates into. + /// + /// is null. + /// + /// + /// The "smime-type" parameter on the Content-Type header is not "certs-only". + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public void Import (SecureMimeContext ctx) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (SecureMimeType != SecureMimeType.CertsOnly && SecureMimeType != SecureMimeType.Unknown) + throw new InvalidOperationException (); + + using (var memory = new MemoryBlockStream ()) { + Content.DecodeTo (memory); + memory.Position = 0; + + ctx.Import (memory); + } + } + + /// + /// Verify the signed-data and return the unencapsulated . + /// + /// + /// Verifies the signed-data and returns the unencapsulated . + /// + /// The list of digital signatures. + /// The S/MIME context to use for verifying the signature. + /// The unencapsulated entity. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The "smime-type" parameter on the Content-Type header is not "signed-data". + /// + /// + /// The extracted content could not be parsed as a MIME entity. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public DigitalSignatureCollection Verify (SecureMimeContext ctx, out MimeEntity entity, CancellationToken cancellationToken = default (CancellationToken)) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (SecureMimeType != SecureMimeType.SignedData && SecureMimeType != SecureMimeType.Unknown) + throw new InvalidOperationException (); + + using (var memory = new MemoryBlockStream ()) { + Content.DecodeTo (memory); + memory.Position = 0; + + return ctx.Verify (memory, out entity, cancellationToken); + } + } + + /// + /// Verifies the signed-data and returns the unencapsulated . + /// + /// + /// Verifies the signed-data using the default and returns the + /// unencapsulated . + /// + /// The list of digital signatures. + /// The unencapsulated entity. + /// The cancellation token. + /// + /// The "smime-type" parameter on the Content-Type header is not "signed-data". + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public DigitalSignatureCollection Verify (out MimeEntity entity, CancellationToken cancellationToken = default (CancellationToken)) + { + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Verify (ctx, out entity, cancellationToken); + } + + /// + /// Compresses the specified entity. + /// + /// + /// Compresses the specified entity using the specified . + /// Most mail clients, even among those that support S/MIME, do not support compression. + /// + /// The compressed entity. + /// The S/MIME context to use for compressing. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Compress (SecureMimeContext ctx, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + return ctx.Compress (memory); + } + } + + /// + /// Compresses the specified entity. + /// + /// + /// Compresses the specified entity using the default . + /// Most mail clients, even among those that support S/MIME, do not support compression. + /// + /// The compressed entity. + /// The entity. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Compress (MimeEntity entity) + { + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Compress (ctx, entity); + } + + /// + /// Encrypts the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients using the supplied . + /// + /// The encrypted entity. + /// The S/MIME context to use for encrypting. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Encrypt (SecureMimeContext ctx, CmsRecipientCollection recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + return ctx.Encrypt (recipients, memory); + } + } + + /// + /// Encrypts the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients using the default . + /// + /// The encrypted entity. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Encrypt (CmsRecipientCollection recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Encrypt (ctx, recipients, entity); + } + + /// + /// Encrypts the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients using the supplied . + /// + /// The encrypted entity. + /// The S/MIME context to use for encrypting. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// Valid certificates could not be found for one or more of the . + /// + /// + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Encrypt (SecureMimeContext ctx, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + return (ApplicationPkcs7Mime) ctx.Encrypt (recipients, memory); + } + } + + /// + /// Encrypts the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients using the default . + /// + /// The encrypted entity. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// Valid certificates could not be found for one or more of the . + /// + /// + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Encrypt (IEnumerable recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Encrypt (ctx, recipients, entity); + } + + /// + /// Cryptographically signs the specified entity. + /// + /// + /// Signs the entity using the supplied signer and . + /// For better interoperability with other mail clients, you should use + /// + /// instead as the multipart/signed format is supported among a much larger + /// subset of mail client software. + /// + /// The signed entity. + /// The S/MIME context to use for signing. + /// The signer. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Sign (SecureMimeContext ctx, CmsSigner signer, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + return ctx.EncapsulatedSign (signer, memory); + } + } + + /// + /// Cryptographically signs the specified entity. + /// + /// + /// Signs the entity using the supplied signer and the default . + /// For better interoperability with other mail clients, you should use + /// + /// instead as the multipart/signed format is supported among a much larger + /// subset of mail client software. + /// + /// The signed entity. + /// The signer. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Sign (CmsSigner signer, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Sign (ctx, signer, entity); + } + + /// + /// Cryptographically signs the specified entity. + /// + /// + /// Signs the entity using the supplied signer, digest algorithm and . + /// For better interoperability with other mail clients, you should use + /// + /// instead as the multipart/signed format is supported among a much larger + /// subset of mail client software. + /// + /// The signed entity. + /// The S/MIME context to use for signing. + /// The signer. + /// The digest algorithm to use for signing. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Sign (SecureMimeContext ctx, MailboxAddress signer, DigestAlgorithm digestAlgo, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + return ctx.EncapsulatedSign (signer, digestAlgo, memory); + } + } + + /// + /// Cryptographically signs the specified entity. + /// + /// + /// Signs the entity using the supplied signer, digest algorithm and the default + /// . + /// For better interoperability with other mail clients, you should use + /// + /// instead as the multipart/signed format is supported among a much larger + /// subset of mail client software. + /// + /// The signed entity. + /// The signer. + /// The digest algorithm to use for signing. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return Sign (ctx, signer, digestAlgo, entity); + } + + /// + /// Cryptographically signs and encrypts the specified entity. + /// + /// + /// Cryptographically signs entity using the supplied signer and then + /// encrypts the result to the specified recipients. + /// + /// The signed and encrypted entity. + /// The S/MIME context to use for signing and encrypting. + /// The signer. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime SignAndEncrypt (SecureMimeContext ctx, CmsSigner signer, CmsRecipientCollection recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + return Encrypt (ctx, recipients, MultipartSigned.Create (ctx, signer, entity)); + } + + /// + /// Cryptographically signs and encrypts the specified entity. + /// + /// + /// Cryptographically signs entity using the supplied signer and the default + /// and then encrypts the result to the specified recipients. + /// + /// The signed and encrypted entity. + /// The signer. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime SignAndEncrypt (CmsSigner signer, CmsRecipientCollection recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return SignAndEncrypt (ctx, signer, recipients, entity); + } + + /// + /// Cryptographically signs and encrypts the specified entity. + /// + /// + /// Cryptographically signs entity using the supplied signer and then + /// encrypts the result to the specified recipients. + /// + /// The signed and encrypted entity. + /// The S/MIME context to use for signing and encrypting. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// A signing certificate could not be found for . + /// -or- + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime SignAndEncrypt (SecureMimeContext ctx, MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + return Encrypt (ctx, recipients, MultipartSigned.Create (ctx, signer, digestAlgo, entity)); + } + + /// + /// Cryptographically signs and encrypts the specified entity. + /// + /// + /// Cryptographically signs entity using the supplied signer and the default + /// and then encrypts the result to the specified recipients. + /// + /// The signed and encrypted entity. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The entity. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// A signing certificate could not be found for . + /// -or- + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static ApplicationPkcs7Mime SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-mime")) + return SignAndEncrypt (ctx, signer, digestAlgo, recipients, entity); + } + } +} diff --git a/src/MimeKit/Cryptography/ApplicationPkcs7Signature.cs b/src/MimeKit/Cryptography/ApplicationPkcs7Signature.cs new file mode 100644 index 0000000..200d503 --- /dev/null +++ b/src/MimeKit/Cryptography/ApplicationPkcs7Signature.cs @@ -0,0 +1,105 @@ +// +// ApplicationPkcs7Signature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME part with a Content-Type of application/pkcs7-signature. + /// + /// + /// An application/pkcs7-signature part contains detatched pkcs7 signature data + /// and is typically contained within a part. + /// To verify the signature, use one of the + /// Verify + /// methods on the parent multipart/signed part. + /// + public class ApplicationPkcs7Signature : MimePart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public ApplicationPkcs7Signature (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the + /// class with a Content-Type of application/pkcs7-signature. + /// + /// + /// Creates a new MIME part with a Content-Type of application/pkcs7-signature + /// and the as its content. + /// + /// The content stream. + /// + /// is null. + /// + /// + /// does not support reading. + /// -or- + /// does not support seeking. + /// + public ApplicationPkcs7Signature (Stream stream) : base ("application", "pkcs7-signature") + { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + ContentTransferEncoding = ContentEncoding.Base64; + Content = new MimeContent (stream); + FileName = "smime.p7s"; + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitApplicationPkcs7Signature (this); + } + } +} diff --git a/src/MimeKit/Cryptography/ArcSigner.cs b/src/MimeKit/Cryptography/ArcSigner.cs new file mode 100644 index 0000000..4f6fbb3 --- /dev/null +++ b/src/MimeKit/Cryptography/ArcSigner.cs @@ -0,0 +1,729 @@ +// +// ArcSigner.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// An ARC signer. + /// + /// + /// An ARC signer. + /// + /// + /// + /// + public abstract class ArcSigner : DkimSignerBase + { + static readonly string[] ArcShouldNotInclude = { "return-path", "received", "comments", "keywords", "bcc", "resent-bcc", "arc-seal" }; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// + protected ArcSigner (string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : base (domain, selector, algorithm) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The signer's private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a private key. + /// + protected ArcSigner (AsymmetricKeyParameter key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (!key.IsPrivate) + throw new ArgumentException ("The key must be a private key.", nameof (key)); + + PrivateKey = key; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + /// The file containing the private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The file did not contain a private key. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + protected ArcSigner (string fileName, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The file name cannot be empty.", nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + PrivateKey = LoadPrivateKey (stream); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The stream containing the private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The file did not contain a private key. + /// + /// + /// An I/O error occurred. + /// + protected ArcSigner (Stream stream, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + PrivateKey = LoadPrivateKey (stream); + } + + /// + /// Generate an ARC-Authentication-Results header. + /// + /// + /// Generates an ARC-Authentication-Results header. + /// If the returned contains a + /// with a equal to "arc", then the + /// will be used as the cv= tag value + /// in the ARC-Seal header generated by the . + /// + /// + /// + /// + /// The format options. + /// The message to create the ARC-Authentication-Results header for. + /// The cancellation token. + /// The ARC-Authentication-Results header or null if the should not sign the message. + protected abstract AuthenticationResults GenerateArcAuthenticationResults (FormatOptions options, MimeMessage message, CancellationToken cancellationToken); + + /// + /// Asynchronously generate an ARC-Authentication-Results header. + /// + /// + /// Asynchronously generates an ARC-Authentication-Results header. + /// If the returned contains a + /// with a equal to "arc", then the + /// will be used as the cv= tag value + /// in the ARC-Seal header generated by the . + /// + /// + /// + /// + /// The format options. + /// The message to create the ARC-Authentication-Results header for. + /// The cancellation token. + /// The ARC-Authentication-Results header or null if the should not sign the message. + protected abstract Task GenerateArcAuthenticationResultsAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken); + + /// + /// Get the timestamp value. + /// + /// + /// Gets the timestamp to use as the t= value in the ARC-Message-Signature and ARC-Seal headers. + /// + /// A value representing the timestamp value. + protected virtual long GetTimestamp () + { + return (long) (DateTime.UtcNow - DateUtils.UnixEpoch).TotalSeconds; + } + + StringBuilder CreateArcHeaderBuilder (int instance) + { + var value = new StringBuilder (); + + value.AppendFormat ("i={0}", instance.ToString (CultureInfo.InvariantCulture)); + + switch (SignatureAlgorithm) { + case DkimSignatureAlgorithm.Ed25519Sha256: + value.Append ("; a=ed25519-sha256"); + break; + case DkimSignatureAlgorithm.RsaSha256: + value.Append ("; a=rsa-sha256"); + break; + default: + value.Append ("; a=rsa-sha1"); + break; + } + + return value; + } + + Header GenerateArcMessageSignature (FormatOptions options, MimeMessage message, int instance, long t, IList headers) + { + var value = CreateArcHeaderBuilder (instance); + byte[] signature, hash; + Header ams; + + value.AppendFormat ("; d={0}; s={1}", Domain, Selector); + value.AppendFormat ("; c={0}/{1}", + HeaderCanonicalizationAlgorithm.ToString ().ToLowerInvariant (), + BodyCanonicalizationAlgorithm.ToString ().ToLowerInvariant ()); + value.AppendFormat ("; t={0}", t); + + using (var stream = new DkimSignatureStream (CreateSigningContext ())) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + // write the specified message headers + DkimVerifierBase.WriteHeaders (options, message, headers, HeaderCanonicalizationAlgorithm, filtered); + + value.AppendFormat ("; h={0}", string.Join (":", headers.ToArray ())); + + hash = message.HashBody (options, SignatureAlgorithm, BodyCanonicalizationAlgorithm, -1); + value.AppendFormat ("; bh={0}", Convert.ToBase64String (hash)); + value.Append ("; b="); + + ams = new Header (HeaderId.ArcMessageSignature, value.ToString ()); + + switch (HeaderCanonicalizationAlgorithm) { + case DkimCanonicalizationAlgorithm.Relaxed: + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, ams, true); + break; + default: + DkimVerifierBase.WriteHeaderSimple (options, filtered, ams, true); + break; + } + + filtered.Flush (); + } + + signature = stream.GenerateSignature (); + + ams.Value += Convert.ToBase64String (signature); + + return ams; + } + } + + Header GenerateArcSeal (FormatOptions options, int instance, string cv, long t, ArcHeaderSet[] sets, int count, Header aar, Header ams) + { + var value = CreateArcHeaderBuilder (instance); + byte[] signature; + Header seal; + + value.AppendFormat ("; cv={0}", cv); + + value.AppendFormat ("; d={0}; s={1}", Domain, Selector); + value.AppendFormat ("; t={0}", t); + + using (var stream = new DkimSignatureStream (CreateSigningContext ())) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + for (int i = 0; i < count; i++) { + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, sets[i].ArcAuthenticationResult, false); + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, sets[i].ArcMessageSignature, false); + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, sets[i].ArcSeal, false); + } + + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, aar, false); + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, ams, false); + + value.Append ("; b="); + + seal = new Header (HeaderId.ArcSeal, value.ToString ()); + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, seal, true); + + filtered.Flush (); + } + + signature = stream.GenerateSignature (); + + seal.Value += Convert.ToBase64String (signature); + + return seal; + } + } + + async Task ArcSignAsync (FormatOptions options, MimeMessage message, IList headers, bool doAsync, CancellationToken cancellationToken) + { + ArcVerifier.GetArcHeaderSets (message, true, out ArcHeaderSet[] sets, out int count); + AuthenticationResults authres; + int instance = count + 1; + string cv; + + if (count > 0) { + var parameters = sets[count - 1].ArcSealParameters; + + // do not sign if there is already a failed ARC-Seal. + if (!parameters.TryGetValue ("cv", out cv) || cv.Equals ("fail", StringComparison.OrdinalIgnoreCase)) + return; + } + + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + if (doAsync) + authres = await GenerateArcAuthenticationResultsAsync (options, message, cancellationToken).ConfigureAwait (false); + else + authres = GenerateArcAuthenticationResults (options, message, cancellationToken); + + if (authres == null) + return; + + authres.Instance = instance; + + var aar = new Header (HeaderId.ArcAuthenticationResults, authres.ToString ()); + cv = "none"; + + if (count > 0) { + cv = "pass"; + + foreach (var method in authres.Results) { + if (method.Method.Equals ("arc", StringComparison.OrdinalIgnoreCase)) { + cv = method.Result; + break; + } + } + } + + var t = GetTimestamp (); + var ams = GenerateArcMessageSignature (options, message, instance, t, headers); + var seal = GenerateArcSeal (options, instance, cv, t, sets, count, aar, ams); + + message.Headers.Insert (0, aar); + message.Headers.Insert (0, ams); + message.Headers.Insert (0, seal); + } + + Task SignAsync (FormatOptions options, MimeMessage message, IList headers, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + var fields = new string[headers.Count]; + var containsFrom = false; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i] == null) + throw new ArgumentException ("The list of headers cannot contain null.", nameof (headers)); + + if (headers[i].Length == 0) + throw new ArgumentException ("The list of headers cannot contain empty string.", nameof (headers)); + + fields[i] = headers[i].ToLowerInvariant (); + + if (ArcShouldNotInclude.Contains (fields[i])) + throw new ArgumentException (string.Format ("The list of headers to sign SHOULD NOT include the '{0}' header.", headers[i]), nameof (headers)); + + if (fields[i] == "from") + containsFrom = true; + } + + if (!containsFrom) + throw new ArgumentException ("The list of headers to sign MUST include the 'From' header.", nameof (headers)); + + return ArcSignAsync (options, message, fields, doAsync, cancellationToken); + } + + Task SignAsync (FormatOptions options, MimeMessage message, IList headers, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + var fields = new string[headers.Count]; + var containsFrom = false; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i] == HeaderId.Unknown) + throw new ArgumentException ("The list of headers to sign cannot include the 'Unknown' header.", nameof (headers)); + + fields[i] = headers[i].ToHeaderName ().ToLowerInvariant (); + + if (ArcShouldNotInclude.Contains (fields[i])) + throw new ArgumentException (string.Format ("The list of headers to sign SHOULD NOT include the '{0}' header.", headers[i].ToHeaderName ()), nameof (headers)); + + if (headers[i] == HeaderId.From) + containsFrom = true; + } + + if (!containsFrom) + throw new ArgumentException ("The list of headers to sign MUST include the 'From' header.", nameof (headers)); + + return ArcSignAsync (options, message, fields, doAsync, cancellationToken); + } + + /// + /// Digitally sign and seal a message using ARC. + /// + /// + /// Digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public void Sign (FormatOptions options, MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + SignAsync (options, message, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously digitally sign and seal a message using ARC. + /// + /// + /// Asynchronously digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// An awaitable task. + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public Task SignAsync (FormatOptions options, MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return SignAsync (options, message, headers, true, cancellationToken); + } + + /// + /// Digitally sign and seal a message using ARC. + /// + /// + /// Digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public void Sign (MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + Sign (FormatOptions.Default, message, headers, cancellationToken); + } + + /// + /// Asynchronously digitally sign and seal a message using ARC. + /// + /// + /// Asynchronously digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// An awaitable task. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public Task SignAsync (MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return SignAsync (FormatOptions.Default, message, headers, cancellationToken); + } + + /// + /// Digitally sign and seal a message using ARC. + /// + /// + /// Digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public void Sign (FormatOptions options, MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + SignAsync (options, message, headers, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously digitally sign and seal a message using ARC. + /// + /// + /// Asynchronously digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// An awaitable task. + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public Task SignAsync (FormatOptions options, MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return SignAsync (options, message, headers, true, cancellationToken); + } + + /// + /// Digitally sign and seal a message using ARC. + /// + /// + /// Digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public void Sign (MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + Sign (FormatOptions.Default, message, headers, cancellationToken); + } + + /// + /// Asynchronously digitally sign and seal a message using ARC. + /// + /// + /// Asynchronously digitally signs and seals a message using ARC. + /// + /// + /// + /// + /// An awaitable task. + /// The message to sign. + /// The list of header fields to sign. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + /// + /// One or more ARC headers either did not contain an instance tag or the instance tag was invalid. + /// + public Task SignAsync (MimeMessage message, IList headers, CancellationToken cancellationToken = default (CancellationToken)) + { + return SignAsync (FormatOptions.Default, message, headers, cancellationToken); + } + } +} diff --git a/src/MimeKit/Cryptography/ArcVerifier.cs b/src/MimeKit/Cryptography/ArcVerifier.cs new file mode 100644 index 0000000..982929f --- /dev/null +++ b/src/MimeKit/Cryptography/ArcVerifier.cs @@ -0,0 +1,694 @@ +// +// ArcVerifier.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; + +using MimeKit.IO; + +namespace MimeKit.Cryptography { + /// + /// An ARC signature validation result. + /// + /// + /// An ARC signature validation result. + /// + /// + /// + /// + public enum ArcSignatureValidationResult + { + /// + /// No signatures to validate. + /// + None, + + /// + /// The validation passed. + /// + Pass, + + /// + /// The validation failed. + /// + Fail + } + + /// + /// An ARC header validation result. + /// + /// + /// Represents an ARC header and its signature validation result. + /// + /// + /// + /// + public class ArcHeaderValidationResult + { + /// + /// Initialize a new instance of the class. + /// + /// The ARC header. + /// + /// is null. + /// + internal ArcHeaderValidationResult (Header header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + Header = header; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The ARC header. + /// The signature validation result. + /// + /// is null. + /// + public ArcHeaderValidationResult (Header header, ArcSignatureValidationResult signature) : this (header) + { + Signature = signature; + } + + /// + /// Get the signature validation result. + /// + /// + /// Gets the signature validation result. + /// + /// The signature validation result. + public ArcSignatureValidationResult Signature { + get; internal set; + } + + /// + /// Get the ARC header. + /// + /// + /// Gets the ARC header. + /// + /// The ARC header. + public Header Header { + get; private set; + } + } + + /// + /// An ARC validation result. + /// + /// + /// Represents the results of ArcVerifier.Verify + /// or ArcVerifier.VerifyAsync. + /// If no ARC headers are found on the , then the result will be + /// and both and + /// will be null. + /// If ARC headers are found on the but could not be parsed, then the + /// result will be and both + /// and will be null. + /// + /// + /// + /// + public class ArcValidationResult + { + internal ArcValidationResult () + { + Chain = ArcSignatureValidationResult.None; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The signature validation results of the entire chain. + /// The validation results for the ARC-Message-Signature header. + /// The validation results for the ARC-Seal headers. + public ArcValidationResult (ArcSignatureValidationResult chain, ArcHeaderValidationResult messageSignature, ArcHeaderValidationResult[] seals) + { + MessageSignature = messageSignature; + Seals = seals; + Chain = chain; + } + + /// + /// Get the validation results for the ARC-Message-Signature header. + /// + /// + /// Gets the validation results for the ARC-Message-Signature header. + /// + /// The validation results for the ARC-Message-Signature header or null + /// if the ARC-Message-Signature header was not found. + public ArcHeaderValidationResult MessageSignature { + get; internal set; + } + + /// + /// Get the validation results for each of the ARC-Seal headers. + /// + /// + /// Gets the validation results for each of the ARC-Seal headers in + /// their instance order. + /// + /// The array of validation results for the ARC-Seal headers or null + /// if no ARC-Seal headers were found. + public ArcHeaderValidationResult[] Seals { + get; internal set; + } + + /// + /// Get the signature validation results of the entire chain. + /// + /// + /// Gets the signature validation results of the entire chain. + /// + /// + /// + /// + /// The signature validation results of the entire chain. + public ArcSignatureValidationResult Chain { + get; internal set; + } + } + + class ArcHeaderSet + { + public Header ArcAuthenticationResult { get; private set; } + + public Dictionary ArcMessageSignatureParameters { get; private set; } + public Header ArcMessageSignature { get; private set; } + + public Dictionary ArcSealParameters { get; private set; } + public Header ArcSeal { get; private set; } + + public bool Add (Header header, Dictionary parameters) + { + switch (header.Id) { + case HeaderId.ArcAuthenticationResults: + if (ArcAuthenticationResult != null) + return false; + + ArcAuthenticationResult = header; + break; + case HeaderId.ArcMessageSignature: + if (ArcMessageSignature != null) + return false; + + ArcMessageSignatureParameters = parameters; + ArcMessageSignature = header; + break; + case HeaderId.ArcSeal: + if (ArcSeal != null) + return false; + + ArcSealParameters = parameters; + ArcSeal = header; + break; + default: + return false; + } + + return true; + } + } + + /// + /// An ARC verifier. + /// + /// + /// Validates Authenticated Received Chains. + /// + /// + /// + /// + public class ArcVerifier : DkimVerifierBase + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + /// The public key locator. + /// + /// is null. + /// + public ArcVerifier (IDkimPublicKeyLocator publicKeyLocator) : base (publicKeyLocator) + { + } + + static void ValidateArcMessageSignatureParameters (IDictionary parameters, out DkimSignatureAlgorithm algorithm, out DkimCanonicalizationAlgorithm headerAlgorithm, + out DkimCanonicalizationAlgorithm bodyAlgorithm, out string d, out string s, out string q, out string[] headers, out string bh, out string b, out int maxLength) + { + ValidateCommonSignatureParameters ("ARC-Message-Signature", parameters, out algorithm, out headerAlgorithm, out bodyAlgorithm, out d, out s, out q, out headers, out bh, out b, out maxLength); + } + + static void ValidateArcSealParameters (IDictionary parameters, out DkimSignatureAlgorithm algorithm, out string d, out string s, out string q, out string b) + { + ValidateCommonParameters ("ARC-Seal", parameters, out algorithm, out d, out s, out q, out b); + + if (parameters.TryGetValue ("h", out string h)) + throw new FormatException (string.Format ("Malformed ARC-Seal header: the 'h' parameter tag is not allowed.")); + } + + async Task VerifyArcMessageSignatureAsync (FormatOptions options, MimeMessage message, Header arcSignature, Dictionary parameters, bool doAsync, CancellationToken cancellationToken) + { + DkimCanonicalizationAlgorithm headerAlgorithm, bodyAlgorithm; + DkimSignatureAlgorithm signatureAlgorithm; + AsymmetricKeyParameter key; + string d, s, q, bh, b; + string[] headers; + int maxLength; + + ValidateArcMessageSignatureParameters (parameters, out signatureAlgorithm, out headerAlgorithm, out bodyAlgorithm, + out d, out s, out q, out headers, out bh, out b, out maxLength); + + if (!IsEnabled (signatureAlgorithm)) + return false; + + if (doAsync) + key = await PublicKeyLocator.LocatePublicKeyAsync (q, d, s, cancellationToken).ConfigureAwait (false); + else + key = PublicKeyLocator.LocatePublicKey (q, d, s, cancellationToken); + + if ((key is RsaKeyParameters rsa) && rsa.Modulus.BitLength < MinimumRsaKeyLength) + return false; + + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + // first check the body hash (if that's invalid, then the entire signature is invalid) + var hash = Convert.ToBase64String (message.HashBody (options, signatureAlgorithm, bodyAlgorithm, maxLength)); + + if (hash != bh) + return false; + + using (var stream = new DkimSignatureStream (CreateVerifyContext (signatureAlgorithm, key))) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + WriteHeaders (options, message, headers, headerAlgorithm, filtered); + + // now include the ARC-Message-Signature header that we are verifying, + // but only after removing the "b=" signature value. + var header = GetSignedSignatureHeader (arcSignature); + + switch (headerAlgorithm) { + case DkimCanonicalizationAlgorithm.Relaxed: + WriteHeaderRelaxed (options, filtered, header, true); + break; + default: + WriteHeaderSimple (options, filtered, header, true); + break; + } + + filtered.Flush (); + } + + return stream.VerifySignature (b); + } + } + + async Task VerifyArcSealAsync (FormatOptions options, ArcHeaderSet[] sets, int i, bool doAsync, CancellationToken cancellationToken) + { + DkimSignatureAlgorithm algorithm; + AsymmetricKeyParameter key; + string d, s, q, b; + + ValidateArcSealParameters (sets[i].ArcSealParameters, out algorithm, out d, out s, out q, out b); + + if (!IsEnabled (algorithm)) + return false; + + if (doAsync) + key = await PublicKeyLocator.LocatePublicKeyAsync (q, d, s, cancellationToken).ConfigureAwait (false); + else + key = PublicKeyLocator.LocatePublicKey (q, d, s, cancellationToken); + + if ((key is RsaKeyParameters rsa) && rsa.Modulus.BitLength < MinimumRsaKeyLength) + return false; + + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + using (var stream = new DkimSignatureStream (CreateVerifyContext (algorithm, key))) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + for (int j = 0; j < i; j++) { + WriteHeaderRelaxed (options, filtered, sets[j].ArcAuthenticationResult, false); + WriteHeaderRelaxed (options, filtered, sets[j].ArcMessageSignature, false); + WriteHeaderRelaxed (options, filtered, sets[j].ArcSeal, false); + } + + WriteHeaderRelaxed (options, filtered, sets[i].ArcAuthenticationResult, false); + WriteHeaderRelaxed (options, filtered, sets[i].ArcMessageSignature, false); + + // now include the ARC-Seal header that we are verifying, + // but only after removing the "b=" signature value. + var seal = GetSignedSignatureHeader (sets[i].ArcSeal); + + WriteHeaderRelaxed (options, filtered, seal, true); + + filtered.Flush (); + } + + return stream.VerifySignature (b); + } + } + + internal static ArcSignatureValidationResult GetArcHeaderSets (MimeMessage message, bool throwOnError, out ArcHeaderSet[] sets, out int count) + { + ArcHeaderSet set; + + sets = new ArcHeaderSet[50]; + count = 0; + + for (int i = 0; i < message.Headers.Count; i++) { + Dictionary parameters = null; + var header = message.Headers[i]; + int instance; + string value; + + switch (header.Id) { + case HeaderId.ArcAuthenticationResults: + if (!AuthenticationResults.TryParse (header.RawValue, out AuthenticationResults authres)) { + if (throwOnError) + throw new FormatException ("Invalid ARC-Authentication-Results header."); + + return ArcSignatureValidationResult.Fail; + } + + if (!authres.Instance.HasValue) { + if (throwOnError) + throw new FormatException ("Missing instance tag in ARC-Authentication-Results header."); + + return ArcSignatureValidationResult.Fail; + } + + instance = authres.Instance.Value; + + if (instance < 1 || instance > 50) { + if (throwOnError) + throw new FormatException (string.Format ("Invalid instance tag in ARC-Authentication-Results header: i={0}", instance)); + + return ArcSignatureValidationResult.Fail; + } + break; + case HeaderId.ArcMessageSignature: + case HeaderId.ArcSeal: + try { + parameters = ParseParameterTags (header.Id, header.Value); + } catch { + if (throwOnError) + throw; + + return ArcSignatureValidationResult.Fail; + } + + if (!parameters.TryGetValue ("i", out value)) { + if (throwOnError) + throw new FormatException (string.Format ("Missing instance tag in {0} header.", header.Id.ToHeaderName ())); + + return ArcSignatureValidationResult.Fail; + } + + if (!int.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out instance) || instance < 1 || instance > 50) { + if (throwOnError) + throw new FormatException (string.Format ("Invalid instance tag in {0} header: i={1}", header.Id.ToHeaderName (), value)); + + return ArcSignatureValidationResult.Fail; + } + break; + default: + instance = 0; + break; + } + + if (instance == 0) + continue; + + set = sets[instance - 1]; + if (set == null) + sets[instance - 1] = set = new ArcHeaderSet (); + + if (!set.Add (header, parameters)) + return ArcSignatureValidationResult.Fail; + + if (instance > count) + count = instance; + } + + if (count == 0) { + // there are no ARC sets + return ArcSignatureValidationResult.None; + } + + // verify that all ARC sets are complete + for (int i = 0; i < count; i++) { + set = sets[i]; + + if (set == null) { + if (throwOnError) + throw new FormatException (string.Format ("Missing ARC headers for i={0}", i + 1)); + + return ArcSignatureValidationResult.Fail; + } + + if (set.ArcAuthenticationResult == null) { + if (throwOnError) + throw new FormatException (string.Format ("Missing ARC-Authentication-Results header for i={0}", i + 1)); + + return ArcSignatureValidationResult.Fail; + } + + if (set.ArcMessageSignature == null) { + if (throwOnError) + throw new FormatException (string.Format ("Missing ARC-Message-Signature header for i={0}", i + 1)); + + return ArcSignatureValidationResult.Fail; + } + + if (set.ArcSeal == null) { + if (throwOnError) + throw new FormatException (string.Format ("Missing ARC-Seal header for i={0}", i + 1)); + + return ArcSignatureValidationResult.Fail; + } + + if (!set.ArcSealParameters.TryGetValue ("cv", out string cv)) { + if (throwOnError) + throw new FormatException (string.Format ("Missing chain validation tag in ARC-Seal header for i={0}.", i + 1)); + + return ArcSignatureValidationResult.Fail; + } + + // The "cv" value for all ARC-Seal header fields MUST NOT be + // "fail". For ARC Sets with instance values > 1, the values + // MUST be "pass". For the ARC Set with instance value = 1, the + // value MUST be "none". + if (!cv.Equals (i == 0 ? "none" : "pass", StringComparison.Ordinal)) + return ArcSignatureValidationResult.Fail; + } + + return ArcSignatureValidationResult.Pass; + } + + async Task VerifyAsync (FormatOptions options, MimeMessage message, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + var result = new ArcValidationResult (); + + switch (GetArcHeaderSets (message, false, out ArcHeaderSet[] sets, out int count)) { + case ArcSignatureValidationResult.None: return result; + case ArcSignatureValidationResult.Fail: + result.Chain = ArcSignatureValidationResult.Fail; + return result; + } + + int newest = count - 1; + + result.Seals = new ArcHeaderValidationResult[count]; + result.Chain = ArcSignatureValidationResult.Pass; + + // validate the most recent Arc-Message-Signature + try { + var parameters = sets[newest].ArcMessageSignatureParameters; + var header = sets[newest].ArcMessageSignature; + + result.MessageSignature = new ArcHeaderValidationResult (header); + + if (await VerifyArcMessageSignatureAsync (options, message, header, parameters, doAsync, cancellationToken).ConfigureAwait (false)) { + result.MessageSignature.Signature = ArcSignatureValidationResult.Pass; + } else { + result.MessageSignature.Signature = ArcSignatureValidationResult.Fail; + result.Chain = ArcSignatureValidationResult.Fail; + } + } catch { + result.MessageSignature.Signature = ArcSignatureValidationResult.Fail; + result.Chain = ArcSignatureValidationResult.Fail; + } + + // validate all Arc-Seals starting with the most recent and proceeding to the oldest + for (int i = newest; i >= 0; i--) { + result.Seals[i] = new ArcHeaderValidationResult (sets[i].ArcSeal); + + try { + if (await VerifyArcSealAsync (options, sets, i, doAsync, cancellationToken).ConfigureAwait (false)) { + result.Seals[i].Signature = ArcSignatureValidationResult.Pass; + } else { + result.Seals[i].Signature = ArcSignatureValidationResult.Fail; + result.Chain = ArcSignatureValidationResult.Fail; + } + } catch { + result.Seals[i].Signature = ArcSignatureValidationResult.Fail; + result.Chain = ArcSignatureValidationResult.Fail; + } + } + + return result; + } + + /// + /// Verify the ARC signature chain. + /// + /// + /// Verifies the ARC signature chain. + /// + /// + /// + /// + /// The ARC validation result. + /// The formatting options. + /// The message to verify. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public ArcValidationResult Verify (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (options, message, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the ARC signature chain. + /// + /// + /// Asynchronously verifies the ARC signature chain. + /// + /// + /// + /// + /// The ARC validation result. + /// The formatting options. + /// The message to verify. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public Task VerifyAsync (FormatOptions options, MimeMessage message, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (options, message, true, cancellationToken); + } + + /// + /// Verify the ARC signature chain. + /// + /// + /// Verifies the ARC signature chain. + /// + /// + /// + /// + /// The ARC validation result. + /// The message to verify. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public ArcValidationResult Verify (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken)) + { + return Verify (FormatOptions.Default, message, cancellationToken); + } + + /// + /// Asynchronously verify the ARC signature chain. + /// + /// + /// Asynchronously verifies the ARC signature chain. + /// + /// + /// + /// + /// The ARC validation result. + /// The message to verify. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public Task VerifyAsync (MimeMessage message, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (FormatOptions.Default, message, cancellationToken); + } + } +} diff --git a/src/MimeKit/Cryptography/AsymmetricAlgorithmExtensions.cs b/src/MimeKit/Cryptography/AsymmetricAlgorithmExtensions.cs new file mode 100644 index 0000000..a422250 --- /dev/null +++ b/src/MimeKit/Cryptography/AsymmetricAlgorithmExtensions.cs @@ -0,0 +1,373 @@ +// +// AsymmetricAlgorithmExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; + +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MimeKit.Cryptography +{ + /// + /// Extension methods for System.Security.Cryptography.AsymmetricAlgorithm. + /// + /// + /// Extension methods for System.Security.Cryptography.AsymmetricAlgorithm. + /// + public static class AsymmetricAlgorithmExtensions + { + static void GetAsymmetricKeyParameters (DSA dsa, bool publicOnly, out AsymmetricKeyParameter pub, out AsymmetricKeyParameter key) + { + var dp = dsa.ExportParameters (!publicOnly); + var validationParameters = dp.Seed != null ? new DsaValidationParameters (dp.Seed, dp.Counter) : null; + var parameters = new DsaParameters ( + new BigInteger (1, dp.P), + new BigInteger (1, dp.Q), + new BigInteger (1, dp.G), + validationParameters); + + pub = new DsaPublicKeyParameters (new BigInteger (1, dp.Y), parameters); + key = publicOnly ? null : new DsaPrivateKeyParameters (new BigInteger (1, dp.X), parameters); + } + + static AsymmetricKeyParameter GetAsymmetricKeyParameter (DSACryptoServiceProvider dsa) + { + GetAsymmetricKeyParameters (dsa, dsa.PublicOnly, out var pub, out var key); + + return dsa.PublicOnly ? pub : key; + } + + static AsymmetricCipherKeyPair GetAsymmetricCipherKeyPair (DSACryptoServiceProvider dsa) + { + if (dsa.PublicOnly) + throw new ArgumentException ("DSA key is not a private key.", "key"); + + GetAsymmetricKeyParameters (dsa, dsa.PublicOnly, out var pub, out var key); + + return new AsymmetricCipherKeyPair (pub, key); + } + + static AsymmetricKeyParameter GetAsymmetricKeyParameter (DSA dsa) + { + GetAsymmetricKeyParameters (dsa, false, out _, out var key); + + return key; + } + + static AsymmetricCipherKeyPair GetAsymmetricCipherKeyPair (DSA dsa) + { + GetAsymmetricKeyParameters (dsa, false, out var pub, out var key); + + return new AsymmetricCipherKeyPair (pub, key); + } + + static void GetAsymmetricKeyParameters (RSA rsa, bool publicOnly, out AsymmetricKeyParameter pub, out AsymmetricKeyParameter key) + { + var rp = rsa.ExportParameters (!publicOnly); + var modulus = new BigInteger (1, rp.Modulus); + var exponent = new BigInteger (1, rp.Exponent); + + pub = new RsaKeyParameters (false, modulus, exponent); + key = publicOnly ? null : new RsaPrivateCrtKeyParameters ( + modulus, + exponent, + new BigInteger (1, rp.D), + new BigInteger (1, rp.P), + new BigInteger (1, rp.Q), + new BigInteger (1, rp.DP), + new BigInteger (1, rp.DQ), + new BigInteger (1, rp.InverseQ) + ); + } + + static AsymmetricKeyParameter GetAsymmetricKeyParameter (RSACryptoServiceProvider rsa) + { + GetAsymmetricKeyParameters (rsa, rsa.PublicOnly, out var pub, out var key); + + return rsa.PublicOnly ? pub : key; + } + + static AsymmetricCipherKeyPair GetAsymmetricCipherKeyPair (RSACryptoServiceProvider rsa) + { + if (rsa.PublicOnly) + throw new ArgumentException ("RSA key is not a private key.", "key"); + + GetAsymmetricKeyParameters (rsa, rsa.PublicOnly, out var pub, out var key); + + return new AsymmetricCipherKeyPair (pub, key); + } + + static AsymmetricKeyParameter GetAsymmetricKeyParameter (RSA rsa) + { + GetAsymmetricKeyParameters (rsa, false, out _, out var key); + + return key; + } + + static AsymmetricCipherKeyPair GetAsymmetricCipherKeyPair (RSA rsa) + { + GetAsymmetricKeyParameters (rsa, false, out var pub, out var key); + + return new AsymmetricCipherKeyPair (pub, key); + } + + /// + /// Convert an AsymmetricAlgorithm into a BouncyCastle AsymmetricKeyParameter. + /// + /// + /// Converts an AsymmetricAlgorithm into a BouncyCastle AsymmetricKeyParameter. + /// Currently, only RSA and DSA keys are supported. + /// + /// The Bouncy Castle AsymmetricKeyParameter. + /// The key. + /// + /// is null. + /// + /// + /// is an unsupported asymmetric algorithm. + /// + public static AsymmetricKeyParameter AsAsymmetricKeyParameter (this AsymmetricAlgorithm key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (key is RSACryptoServiceProvider rsaKey) + return GetAsymmetricKeyParameter (rsaKey); + + if (key is RSA rsa) + return GetAsymmetricKeyParameter (rsa); + + if (key is DSACryptoServiceProvider dsaKey) + return GetAsymmetricKeyParameter (dsaKey); + + if (key is DSA dsa) + return GetAsymmetricKeyParameter (dsa); + + // TODO: support ECDiffieHellman and ECDsa? + + throw new NotSupportedException (string.Format ("'{0}' is currently not supported.", key.GetType ().Name)); + } + + /// + /// Convert an AsymmetricAlgorithm into a BouncyCastle AsymmetricCipherKeyPair. + /// + /// + /// Converts an AsymmetricAlgorithm into a BouncyCastle AsymmetricCipherKeyPair. + /// Currently, only RSA and DSA keys are supported. + /// + /// The Bouncy Castle AsymmetricCipherKeyPair. + /// The key. + /// + /// is null. + /// + /// + /// is a public key. + /// + /// + /// is an unsupported asymmetric algorithm. + /// + public static AsymmetricCipherKeyPair AsAsymmetricCipherKeyPair (this AsymmetricAlgorithm key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (key is RSACryptoServiceProvider rsaKey) + return GetAsymmetricCipherKeyPair (rsaKey); + + if (key is RSA rsa) + return GetAsymmetricCipherKeyPair (rsa); + + if (key is DSACryptoServiceProvider dsaKey) + return GetAsymmetricCipherKeyPair (dsaKey); + + if (key is DSA dsa) + return GetAsymmetricCipherKeyPair (dsa); + + // TODO: support ECDiffieHellman and ECDsa? + + throw new NotSupportedException (string.Format ("'{0}' is currently not supported.", key.GetType ().Name)); + } + + static byte[] GetPaddedByteArray (BigInteger big, int length) + { + var bytes = big.ToByteArrayUnsigned (); + + if (bytes.Length >= length) + return bytes; + + var padded = new byte[length]; + + Buffer.BlockCopy (bytes, 0, padded, length - bytes.Length, bytes.Length); + + return padded; + } + + static DSAParameters GetDSAParameters (DsaKeyParameters key) + { + var parameters = new DSAParameters (); + + if (key.Parameters.ValidationParameters != null) { + parameters.Counter = key.Parameters.ValidationParameters.Counter; + parameters.Seed = key.Parameters.ValidationParameters.GetSeed (); + } + + parameters.G = key.Parameters.G.ToByteArrayUnsigned (); + parameters.P = key.Parameters.P.ToByteArrayUnsigned (); + parameters.Q = key.Parameters.Q.ToByteArrayUnsigned (); + + return parameters; + } + + static AsymmetricAlgorithm GetAsymmetricAlgorithm (DsaPrivateKeyParameters key, DsaPublicKeyParameters pub) + { + var parameters = GetDSAParameters (key); + parameters.X = key.X.ToByteArrayUnsigned (); + + if (pub != null) + parameters.Y = pub.Y.ToByteArrayUnsigned (); + + var dsa = new DSACryptoServiceProvider (); + + dsa.ImportParameters (parameters); + + return dsa; + } + + static AsymmetricAlgorithm GetAsymmetricAlgorithm (DsaPublicKeyParameters key) + { + var parameters = GetDSAParameters (key); + parameters.Y = key.Y.ToByteArrayUnsigned (); + + var dsa = new DSACryptoServiceProvider (); + + dsa.ImportParameters (parameters); + + return dsa; + } + + static AsymmetricAlgorithm GetAsymmetricAlgorithm (RsaPrivateCrtKeyParameters key) + { + var parameters = new RSAParameters (); + + parameters.Exponent = key.PublicExponent.ToByteArrayUnsigned (); + parameters.Modulus = key.Modulus.ToByteArrayUnsigned (); + parameters.P = key.P.ToByteArrayUnsigned (); + parameters.Q = key.Q.ToByteArrayUnsigned (); + + parameters.InverseQ = GetPaddedByteArray (key.QInv, parameters.Q.Length); + parameters.D = GetPaddedByteArray (key.Exponent, parameters.Modulus.Length); + parameters.DP = GetPaddedByteArray (key.DP, parameters.P.Length); + parameters.DQ = GetPaddedByteArray (key.DQ, parameters.Q.Length); + + var rsa = new RSACryptoServiceProvider (); + + rsa.ImportParameters (parameters); + + return rsa; + } + + static AsymmetricAlgorithm GetAsymmetricAlgorithm (RsaKeyParameters key) + { + var parameters = new RSAParameters (); + parameters.Exponent = key.Exponent.ToByteArrayUnsigned (); + parameters.Modulus = key.Modulus.ToByteArrayUnsigned (); + + var rsa = new RSACryptoServiceProvider (); + + rsa.ImportParameters (parameters); + + return rsa; + } + + /// + /// Convert a BouncyCastle AsymmetricKeyParameter into an AsymmetricAlgorithm. + /// + /// + /// Converts a BouncyCastle AsymmetricKeyParameter into an AsymmetricAlgorithm. + /// Currently, only RSA and DSA keys are supported. + /// + /// The AsymmetricAlgorithm. + /// The AsymmetricKeyParameter. + /// + /// is null. + /// + /// + /// is an unsupported asymmetric key parameter. + /// + public static AsymmetricAlgorithm AsAsymmetricAlgorithm (this AsymmetricKeyParameter key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (key.IsPrivate) { + if (key is RsaPrivateCrtKeyParameters rsaPrivateKey) + return GetAsymmetricAlgorithm (rsaPrivateKey); + + if (key is DsaPrivateKeyParameters dsaPrivateKey) + return GetAsymmetricAlgorithm (dsaPrivateKey, null); + } else { + if (key is RsaKeyParameters rsaPublicKey) + return GetAsymmetricAlgorithm (rsaPublicKey); + + if (key is DsaPublicKeyParameters dsaPublicKey) + return GetAsymmetricAlgorithm (dsaPublicKey); + } + + throw new NotSupportedException (string.Format ("{0} is currently not supported.", key.GetType ().Name)); + } + + /// + /// Convert a BouncyCastle AsymmetricCipherKeyPair into an AsymmetricAlgorithm. + /// + /// + /// Converts a BouncyCastle AsymmetricCipherKeyPair into an AsymmetricAlgorithm. + /// Currently, only RSA and DSA keys are supported. + /// + /// The AsymmetricAlgorithm. + /// The AsymmetricCipherKeyPair. + /// + /// is null. + /// + /// + /// is an unsupported asymmetric algorithm. + /// + public static AsymmetricAlgorithm AsAsymmetricAlgorithm (this AsymmetricCipherKeyPair key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (key.Private is RsaPrivateCrtKeyParameters rsaPrivateKey) + return GetAsymmetricAlgorithm (rsaPrivateKey); + + if (key.Private is DsaPrivateKeyParameters dsaPrivateKey) + return GetAsymmetricAlgorithm (dsaPrivateKey, (DsaPublicKeyParameters) key.Public); + + throw new NotSupportedException (string.Format ("{0} is currently not supported.", key.GetType ().Name)); + } + } +} diff --git a/src/MimeKit/Cryptography/AuthenticationResults.cs b/src/MimeKit/Cryptography/AuthenticationResults.cs new file mode 100644 index 0000000..f2ea9c1 --- /dev/null +++ b/src/MimeKit/Cryptography/AuthenticationResults.cs @@ -0,0 +1,1366 @@ +// +// AuthenticationResults.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Globalization; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// A parsed representation of the Authentication-Results header. + /// + /// + /// The Authentication-Results header is used with electronic mail messages to + /// indicate the results of message authentication efforts. Any receiver-side + /// software, such as mail filters or Mail User Agents (MUAs), can use this header + /// field to relay that information in a convenient and meaningful way to users or + /// to make sorting and filtering decisions. + /// + public class AuthenticationResults + { + AuthenticationResults () + { + Results = new List (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The authentication service identifier. + /// + /// is null. + /// + public AuthenticationResults (string authservid) : this () + { + if (authservid == null) + throw new ArgumentNullException (nameof (authservid)); + + AuthenticationServiceIdentifier = authservid; + } + + /// + /// Get the authentication service identifier. + /// + /// + /// Gets the authentication service identifier. + /// The authentication service identifier is the authserv-id token + /// as defined in rfc7601. + /// + /// The authserv-id token. + public string AuthenticationServiceIdentifier { + get; private set; + } + + /// + /// Get or set the instance value. + /// + /// + /// Gets or sets the instance value. + /// This value will only be set if the + /// represents an ARC-Authentication-Results header value. + /// + /// The instance. + public int? Instance { + get; set; + } + + /// + /// Get or set the Authentication-Results version. + /// + /// + /// Gets or sets the Authentication-Results version. + /// The version value is the authres-version token as defined in + /// rfc7601. + /// + /// The authres-version token. + public int? Version { + get; set; + } + + /// + /// Get the list of authentication results. + /// + /// + /// Gets the list of authentication results. + /// + /// The list of authentication results. + public List Results { + get; private set; + } + + internal void Encode (FormatOptions options, StringBuilder builder, int lineLength) + { + int space = 1; + + if (Instance.HasValue) { + var i = Instance.Value.ToString (CultureInfo.InvariantCulture); + + builder.AppendFormat (" i={0};", i); + lineLength += 4 + i.Length; + } + + if (AuthenticationServiceIdentifier != null) { + if (lineLength + space + AuthenticationServiceIdentifier.Length > options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + space = 0; + } + + if (space > 0) { + builder.Append (' '); + lineLength++; + } + + builder.Append (AuthenticationServiceIdentifier); + lineLength += AuthenticationServiceIdentifier.Length; + + if (Version.HasValue) { + var version = Version.Value.ToString (CultureInfo.InvariantCulture); + + if (lineLength + 1 + version.Length > options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + lineLength += version.Length; + builder.Append (version); + } + + builder.Append (';'); + lineLength++; + } + + if (Results.Count > 0) { + for (int i = 0; i < Results.Count; i++) { + if (i > 0) { + builder.Append (';'); + lineLength++; + } + + Results[i].Encode (options, builder, ref lineLength); + } + } else { + builder.Append (" none"); + } + + builder.Append (options.NewLine); + } + + /// + /// Serializes the to a string. + /// + /// + /// Creates a string-representation of the . + /// + /// The serialized string. + public override string ToString () + { + var builder = new StringBuilder (); + + if (Instance.HasValue) + builder.AppendFormat ("i={0}; ", Instance.Value.ToString (CultureInfo.InvariantCulture)); + + if (AuthenticationServiceIdentifier != null) { + builder.Append (AuthenticationServiceIdentifier); + + if (Version.HasValue) { + builder.Append (' '); + builder.Append (Version.Value.ToString (CultureInfo.InvariantCulture)); + } + + builder.Append ("; "); + } + + if (Results.Count > 0) { + for (int i = 0; i < Results.Count; i++) { + if (i > 0) + builder.Append ("; "); + builder.Append (Results[i]); + } + } else { + builder.Append ("none"); + } + + return builder.ToString (); + } + + static bool IsKeyword (byte c) + { + return (c >= (byte) 'A' && c <= (byte) 'Z') || + (c >= (byte) 'a' && c <= (byte) 'z') || + (c >= (byte) '0' && c <= (byte) '9') || + c == (byte) '-'; + } + + static bool SkipKeyword (byte[] text, ref int index, int endIndex) + { + int startIndex = index; + + while (index < endIndex && IsKeyword (text[index])) + index++; + + return index > startIndex; + } + + static bool SkipValue (byte[] text, ref int index, int endIndex, out bool quoted) + { + if (text[index] == (byte) '"') { + quoted = true; + + if (!ParseUtils.SkipQuoted (text, ref index, endIndex, false)) + return false; + } else { + quoted = false; + + if (!ParseUtils.SkipToken (text, ref index, endIndex)) + return false; + } + + return true; + } + + static bool SkipDomain (byte[] text, ref int index, int endIndex) + { + int startIndex = index; + + while (ParseUtils.SkipAtom (text, ref index, endIndex) && index < endIndex && text[index] == (byte) '.') + index++; + + if (index > startIndex && text[index - 1] != (byte) '.') + return true; + + return false; + } + + // pvalue := [CFWS] ( value / [ [ local-part ] "@" ] domain-name ) [CFWS] + // value := token / quoted-string + // token := 1* + // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" + static bool IsPValueToken (byte c) + { + // Note: We're allowing '/' because it is a base64 character + // + // See https://github.com/jstedfast/MimeKit/issues/518 for details. + return c.IsToken () || c == (byte) '/'; + } + + static void SkipPValueToken (byte[] text, ref int index, int endIndex) + { + while (index < endIndex && IsPValueToken (text[index])) + index++; + } + + static bool SkipPropertyValue (byte[] text, ref int index, int endIndex, out bool quoted) + { + // pvalue := [CFWS] ( value / [ [ local-part ] "@" ] domain-name ) [CFWS] + // value := token / quoted-string + // token := 1* + // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" + if (text[index] == (byte) '"') { + // quoted-string + quoted = true; + + if (!ParseUtils.SkipQuoted (text, ref index, endIndex, false)) + return false; + + return true; + } + + quoted = false; + + if (text[index] == (byte) '@') { + // "@" domain-name + index++; + + if (!SkipDomain (text, ref index, endIndex)) + return false; + + return true; + } + + SkipPValueToken (text, ref index, endIndex); + + if (index < endIndex) { + if (text[index] == (byte) '@') { + // local-part@domain-name + index++; + + if (!SkipDomain (text, ref index, endIndex)) + return false; + } + } + + return true; + } + + static bool TryParseMethods (byte[] text, ref int index, int endIndex, bool throwOnError, AuthenticationResults authres) + { + string value; + bool quoted; + + while (index < endIndex) { + string srvid = null; + + method_token: + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + break; + + int methodIndex = index; + + // skip the method name + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid method token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + // Note: Office365 seems to (sometimes) place a method-specific authserv-id token before each + // method. This block of code is here to handle that case. + // + // See https://github.com/jstedfast/MimeKit/issues/527 for details. + if (srvid == null && index < endIndex && text[index] == '.') { + index = methodIndex; + + if (!SkipDomain (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid Office365 authserv-id token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + srvid = Encoding.UTF8.GetString (text, methodIndex, index - methodIndex); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Missing semi-colon after Office365 authserv-id token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected token after Office365 authserv-id token at offset {0}", index), index, index); + + return false; + } + + // skip over ';' + index++; + + goto method_token; + } + + var method = Encoding.ASCII.GetString (text, methodIndex, index - methodIndex); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (method != "none") { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete methodspec token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + if (authres.Results.Count > 0) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid no-result token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + break; + } + + var resinfo = new AuthenticationMethodResult (method); + resinfo.Office365AuthenticationServiceIdentifier = srvid; + authres.Results.Add (resinfo); + + int tokenIndex; + + if (text[index] == (byte) '/') { + // optional method-version token + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + tokenIndex = index; + + if (!ParseUtils.TryParseInt32 (text, ref index, endIndex, out int version)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid method-version token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + resinfo.Version = version; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete methodspec token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + } + + if (text[index] != (byte) '=') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid methodspec token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + // skip over '=' + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete methodspec token at offset {0}", methodIndex), methodIndex, index); + + return false; + } + + tokenIndex = index; + + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid result token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + resinfo.Result = Encoding.ASCII.GetString (text, tokenIndex, index - tokenIndex); + + ParseUtils.SkipWhiteSpace (text, ref index, endIndex); + + if (index < endIndex && text[index] == (byte) '(') { + int commentIndex = index; + + if (!ParseUtils.SkipComment (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete comment token at offset {0}", commentIndex), commentIndex, index); + + return false; + } + + commentIndex++; + + resinfo.ResultComment = Header.Unfold (Encoding.UTF8.GetString (text, commentIndex, (index - 1) - commentIndex)); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + } + + if (index >= endIndex) + break; + + if (text[index] == (byte) ';') { + index++; + continue; + } + + // optional reasonspec or propspec + tokenIndex = index; + + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid reasonspec or propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + value = Encoding.ASCII.GetString (text, tokenIndex, index - tokenIndex); + + if (value == "reason" || value == "action") { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete {0}spec token at offset {1}", value, tokenIndex), tokenIndex, index); + + return false; + } + + if (text[index] != (byte) '=') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid {0}spec token at offset {1}", value, tokenIndex), tokenIndex, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + int reasonIndex = index; + + if (index >= endIndex || !SkipValue (text, ref index, endIndex, out quoted)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid {0}spec value token at offset {1}", value, reasonIndex), reasonIndex, index); + + return false; + } + + var reason = Encoding.UTF8.GetString (text, reasonIndex, index - reasonIndex); + + if (quoted) + reason = MimeUtils.Unquote (reason); + + if (value == "action") + resinfo.Action = reason; + else + resinfo.Reason = reason; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + break; + + if (text[index] == (byte) ';') { + index++; + continue; + } + + // optional propspec + tokenIndex = index; + + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + value = Encoding.ASCII.GetString (text, tokenIndex, index - tokenIndex); + } + + do { + // value is a propspec ptype token + var ptype = value; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + if (text[index] != (byte) '.') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + int propertyIndex = index; + + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid property token at offset {0}", propertyIndex), propertyIndex, index); + + return false; + } + + var property = Encoding.ASCII.GetString (text, propertyIndex, index - propertyIndex); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + if (text[index] != (byte) '=') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + int valueIndex = index; + + if (index >= text.Length || !SkipPropertyValue (text, ref index, endIndex, out quoted)) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + value = Encoding.UTF8.GetString (text, valueIndex, index - valueIndex); + + if (quoted) + value = MimeUtils.Unquote (value); + + var propspec = new AuthenticationMethodProperty (ptype, property, value, quoted); + resinfo.Properties.Add (propspec); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || text[index] == (byte) ';') + break; + + tokenIndex = index; + + if (!SkipKeyword (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid propspec token at offset {0}", tokenIndex), tokenIndex, index); + + return false; + } + + value = Encoding.ASCII.GetString (text, tokenIndex, index - tokenIndex); + } while (true); + + // skip over ';' + index++; + } + + return true; + } + + static bool TryParse (byte[] text, ref int index, int endIndex, bool throwOnError, out AuthenticationResults authres) + { + int? instance = null; + string srvid = null; + string value; + bool quoted; + + authres = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + do { + int start = index; + + if (index >= endIndex || !SkipValue (text, ref index, endIndex, out quoted)) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete authserv-id token at offset {0}", start), start, index); + + return false; + } + + value = Encoding.UTF8.GetString (text, start, index - start); + + if (quoted) { + // this can only be the authserv-id token + srvid = MimeUtils.Unquote (value); + } else { + // this could either be the authserv-id or it could be "i=#" (ARC instance) + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index < endIndex && text[index] == (byte) '=') { + // probably i=# + if (instance.HasValue) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid token at offset {0}", start), start, index); + + return false; + } + + if (value != "i") { + // Office 365 Authentication-Results do not include an authserv-id token, so this is probably a method. + // Rewind the parser and start over again with the assumption that the Authentication-Results only + // contains methods. + // + // See https://github.com/jstedfast/MimeKit/issues/490 for details. + + authres = new AuthenticationResults (); + index = 0; + + return TryParseMethods (text, ref index, endIndex, throwOnError, authres); + } + + // skip over '=' + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + start = index; + + if (!ParseUtils.TryParseInt32 (text, ref index, endIndex, out int i)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid instance value at offset {0}", start), start, index); + + return false; + } + + instance = i; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Missing semi-colon after instance value at offset {0}", start), start, index); + + return false; + } + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected token after instance value at offset {0}", index), index, index); + + return false; + } + + // skip over ';' + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + } else { + srvid = value; + } + } + } while (srvid == null); + + authres = new AuthenticationResults (srvid) { Instance = instance }; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + return true; + + if (text[index] != (byte) ';') { + // might be the authres-version token + int start = index; + + if (!ParseUtils.TryParseInt32 (text, ref index, endIndex, out int version)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid authres-version at offset {0}", start), start, index); + + return false; + } + + authres.Version = version; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + return true; + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Unknown token at offset {0}", index), index, index); + + return false; + } + } + + // skip the ';' + index++; + + return TryParseMethods (text, ref index, endIndex, throwOnError, authres); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses an Authentication-Results header value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true if the authentication results were successfully parsed; otherwise, false. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed authentication results. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out AuthenticationResults authres) + { + ParseUtils.ValidateArguments (buffer, startIndex, length); + + int index = startIndex; + + return TryParse (buffer, ref index, startIndex + length, false, out authres); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses an Authentication-Results header value from the supplied buffer. + /// + /// true if the authentication results were successfully parsed; otherwise, false. + /// The input buffer. + /// The parsed authentication results. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out AuthenticationResults authres) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + int index = 0; + + return TryParse (buffer, ref index, buffer.Length, false, out authres); + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses an Authentication-Results header value from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The input buffer. + /// The start index of the buffer. + /// The length of the buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The could not be parsed. + /// + public static AuthenticationResults Parse (byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (buffer, startIndex, length); + + AuthenticationResults authres; + int index = startIndex; + + TryParse (buffer, ref index, startIndex + length, true, out authres); + + return authres; + } + + /// + /// Parse the specified input buffer into a new instance of the class. + /// + /// + /// Parses an Authentication-Results header value from the supplied buffer. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// The could not be parsed. + /// + public static AuthenticationResults Parse (byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + AuthenticationResults authres; + int index = 0; + + TryParse (buffer, ref index, buffer.Length, true, out authres); + + return authres; + } + } + + /// + /// An authentication method results. + /// + /// + /// An authentication method results. + /// + public class AuthenticationMethodResult + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The method used for authentication. + /// + /// is null. + /// + internal AuthenticationMethodResult (string method) + { + if (method == null) + throw new ArgumentNullException (nameof (method)); + + Properties = new List (); + Method = method; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The method used for authentication. + /// The result of the authentication method. + /// + /// is null. + /// -or- + /// is null. + /// + public AuthenticationMethodResult (string method, string result) : this (method) + { + if (result == null) + throw new ArgumentNullException (nameof (result)); + + Result = result; + } + + /// + /// Get the Office365 method-specific authserv-id. + /// + /// + /// Gets the Office365 method-specific authserv-id. + /// An authentication service identifier is the authserv-id token + /// as defined in rfc7601. + /// Instead of specifying a single authentication service identifier at the + /// beginning of the header value, Office365 seems to provide a different + /// authentication service identifier for each method. + /// + /// The authserv-id token. + public string Office365AuthenticationServiceIdentifier { + get; internal set; + } + + /// + /// Get the authentication method. + /// + /// + /// Gets the authentication method. + /// + /// The authentication method. + public string Method { + get; private set; + } + + /// + /// Get the authentication method version. + /// + /// + /// Gets the authentication method version. + /// + /// The authentication method version. + public int? Version { + get; set; + } + + /// + /// Get the authentication method results. + /// + /// + /// Gets the authentication method results. + /// + /// The authentication method results. + public string Result { + get; internal set; + } + + /// + /// Get the comment regarding the authentication method result. + /// + /// + /// Gets the comment regarding the authentication method result. + /// + /// The comment regarding the authentication method result. + public string ResultComment { + get; set; + } + + /// + /// Get the action taken for the authentication method result. + /// + /// + /// Gets the action taken for the authentication method result. + /// + /// The action taken for the authentication method result. + public string Action { + get; internal set; + } + + /// + /// Get the reason for the authentication method result. + /// + /// + /// Gets the reason for the authentication method result. + /// + /// The reason for the authentication method result. + public string Reason { + get; set; + } + + /// + /// Get the properties used by the authentication method. + /// + /// + /// Gets the properties used by the authentication method. + /// + /// The properties used by the authentication method. + public List Properties { + get; private set; + } + + internal void Encode (FormatOptions options, StringBuilder builder, ref int lineLength) + { + // try to put the entire result on 1 line + var complete = ToString (); + + if (complete.Length + 1 < options.MaxLineLength) { + // if it fits, it sits... + if (lineLength + complete.Length + 1 > options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + lineLength += complete.Length; + builder.Append (complete); + return; + } + + // Note: if we've made it this far, then we can't put everything on one line... + + var tokens = new List (); + tokens.Add (" "); + + if (Office365AuthenticationServiceIdentifier != null) { + tokens.Add (Office365AuthenticationServiceIdentifier); + tokens.Add (";"); + tokens.Add (" "); + } + + if (Version.HasValue) { + var version = Version.Value.ToString (CultureInfo.InvariantCulture); + + if (Method.Length + 1 + version.Length + 1 + Result.Length < options.MaxLineLength) { + tokens.Add ($"{Method}/{version}={Result}"); + } else if (Method.Length + 1 + version.Length < options.MaxLineLength) { + tokens.Add ($"{Method}/{version}"); + tokens.Add ("="); + tokens.Add (Result); + } else { + tokens.Add (Method); + tokens.Add ("/"); + tokens.Add (version); + tokens.Add ("="); + tokens.Add (Result); + } + } else { + if (Method.Length + 1 + Result.Length < options.MaxLineLength) { + tokens.Add ($"{Method}={Result}"); + } else { + // we will have to break this up into individual tokens + tokens.Add (Method); + tokens.Add ("="); + tokens.Add (Result); + } + } + + if (!string.IsNullOrEmpty (ResultComment)) { + tokens.Add (" "); + tokens.Add ($"({ResultComment})"); + } + + if (!string.IsNullOrEmpty (Reason)) { + var reason = MimeUtils.Quote (Reason); + + tokens.Add (" "); + + if ("reason=".Length + reason.Length < options.MaxLineLength) { + tokens.Add ($"reason={reason}"); + } else { + tokens.Add ("reason="); + tokens.Add (reason); + } + } else if (!string.IsNullOrEmpty (Action)) { + var action = MimeUtils.Quote (Action); + + tokens.Add (" "); + + if ("action=".Length + action.Length < options.MaxLineLength) { + tokens.Add ($"action={action}"); + } else { + tokens.Add ("action="); + tokens.Add (action); + } + } + + for (int i = 0; i < Properties.Count; i++) + Properties[i].AppendTokens (options, tokens); + + builder.AppendTokens (options, ref lineLength, tokens); + } + + /// + /// Serializes the to a string. + /// + /// + /// Creates a string-representation of the . + /// + /// The serialized string. + public override string ToString () + { + var builder = new StringBuilder (); + + if (Office365AuthenticationServiceIdentifier != null) { + builder.Append (Office365AuthenticationServiceIdentifier); + builder.Append ("; "); + } + + builder.Append (Method); + + if (Version.HasValue) { + builder.Append ('/'); + builder.Append (Version.Value.ToString (CultureInfo.InvariantCulture)); + } + + builder.Append ('='); + builder.Append (Result); + + if (!string.IsNullOrEmpty (ResultComment)) { + builder.Append (" ("); + builder.Append (ResultComment); + builder.Append (')'); + } + + if (!string.IsNullOrEmpty (Reason)) { + builder.Append (" reason="); + builder.Append (MimeUtils.Quote (Reason)); + } else if (!string.IsNullOrEmpty (Action)) { + builder.Append (" action="); + builder.Append (MimeUtils.Quote (Action)); + } + + for (int i = 0; i < Properties.Count; i++) { + builder.Append (' '); + builder.Append (Properties[i]); + } + + return builder.ToString (); + } + } + + /// + /// An authentication method property. + /// + /// + /// An authentication method property. + /// + public class AuthenticationMethodProperty + { + static readonly char[] TokenSpecials = ByteExtensions.TokenSpecials.ToCharArray (); + bool? quoted; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The property type. + /// The name of the property. + /// The value of the property. + /// true if the property value was originally quoted; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + internal AuthenticationMethodProperty (string ptype, string property, string value, bool? quoted) + { + if (ptype == null) + throw new ArgumentNullException (nameof (ptype)); + + if (property == null) + throw new ArgumentNullException (nameof (property)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + this.quoted = quoted; + PropertyType = ptype; + Property = property; + Value = value; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The property type. + /// The name of the property. + /// The value of the property. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public AuthenticationMethodProperty (string ptype, string property, string value) : this (ptype, property, value, null) + { + } + + /// + /// Get the type of the property. + /// + /// + /// Gets the type of the property. + /// + /// The type of the property. + public string PropertyType { + get; private set; + } + + /// + /// Get the property name. + /// + /// + /// Gets the property name. + /// + /// The name of the property. + public string Property { + get; private set; + } + + /// + /// Get the property value. + /// + /// + /// Gets the property value. + /// + /// The value of the property. + public string Value { + get; private set; + } + + internal void AppendTokens (FormatOptions options, List tokens) + { + var quote = quoted.HasValue ? quoted.Value : Value.IndexOfAny (TokenSpecials) != -1; + var value = quote ? MimeUtils.Quote (Value) : Value; + + tokens.Add (" "); + + if (PropertyType.Length + 1 + Property.Length + 1 + value.Length < options.MaxLineLength) { + tokens.Add ($"{PropertyType}.{Property}={value}"); + } else if (PropertyType.Length + 1 + Property.Length + 1 < options.MaxLineLength) { + tokens.Add ($"{PropertyType}.{Property}="); + tokens.Add (value); + } else { + tokens.Add (PropertyType); + tokens.Add ("."); + tokens.Add (Property); + tokens.Add ("="); + tokens.Add (value); + } + } + + /// + /// Serializes the to a string. + /// + /// + /// Creates a string-representation of the . + /// + /// The serialized string. + public override string ToString () + { + var quote = quoted.HasValue ? quoted.Value : Value.IndexOfAny (TokenSpecials) != -1; + var value = quote ? MimeUtils.Quote (Value) : Value; + + return $"{PropertyType}.{Property}={value}"; + } + } +} diff --git a/src/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs b/src/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs new file mode 100644 index 0000000..292b6d1 --- /dev/null +++ b/src/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs @@ -0,0 +1,365 @@ +// +// BouncyCastleCertificateExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.Smime; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; + +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; + +namespace MimeKit.Cryptography { + /// + /// Extension methods for use with BouncyCastle X509Certificates. + /// + /// + /// Extension methods for use with BouncyCastle X509Certificates. + /// + public static class BouncyCastleCertificateExtensions + { + /// + /// Convert a BouncyCastle certificate into an X509Certificate2. + /// + /// + /// Converts a BouncyCastle certificate into an X509Certificate2. + /// + /// The X509Certificate2. + /// The BouncyCastle certificate. + /// + /// is null. + /// + public static X509Certificate2 AsX509Certificate2 (this X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + return new X509Certificate2 (certificate.GetEncoded ()); + } + + internal static bool IsSelfSigned (this X509Certificate certificate) + { + return certificate.SubjectDN.Equivalent (certificate.IssuerDN); + } + + /// + /// Gets the issuer name info. + /// + /// + /// For a list of available identifiers, see . + /// + /// The issuer name info. + /// The certificate. + /// The name identifier. + /// + /// is null. + /// + public static string GetIssuerNameInfo (this X509Certificate certificate, DerObjectIdentifier identifier) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + // FIXME: GetValueList() should be fixed to return IList + var list = certificate.IssuerDN.GetValueList (identifier); + if (list.Count == 0) + return string.Empty; + + return (string) list[0]; + } + + /// + /// Gets the issuer name info. + /// + /// + /// For a list of available identifiers, see . + /// + /// The issuer name info. + /// The certificate. + /// The name identifier. + /// + /// is null. + /// + public static string GetSubjectNameInfo (this X509Certificate certificate, DerObjectIdentifier identifier) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + // FIXME: GetValueList() should be fixed to return IList + var list = certificate.SubjectDN.GetValueList (identifier); + if (list.Count == 0) + return string.Empty; + + return (string) list[0]; + } + + /// + /// Gets the common name of the certificate. + /// + /// + /// Gets the common name of the certificate. + /// + /// The common name. + /// The certificate. + /// + /// is null. + /// + public static string GetCommonName (this X509Certificate certificate) + { + return certificate.GetSubjectNameInfo (X509Name.CN); + } + + /// + /// Gets the subject name of the certificate. + /// + /// + /// Gets the subject name of the certificate. + /// + /// The subject name. + /// The certificate. + /// + /// is null. + /// + public static string GetSubjectName (this X509Certificate certificate) + { + return certificate.GetSubjectNameInfo (X509Name.Name); + } + + /// + /// Gets the subject email address of the certificate. + /// + /// + /// The email address component of the certificate's Subject identifier is + /// sometimes used as a way of looking up certificates for a particular + /// user if a fingerprint is not available. + /// + /// The subject email address. + /// The certificate. + /// + /// is null. + /// + public static string GetSubjectEmailAddress (this X509Certificate certificate) + { + var address = certificate.GetSubjectNameInfo (X509Name.EmailAddress); + + if (!string.IsNullOrEmpty (address)) + return address; + + var alt = certificate.GetExtensionValue (X509Extensions.SubjectAlternativeName); + + if (alt == null) + return string.Empty; + + var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.GetOctets ())); + + foreach (Asn1Encodable encodable in seq) { + var name = GeneralName.GetInstance (encodable); + + if (name.TagNo == GeneralName.Rfc822Name) + return ((IAsn1String) name.Name).GetString (); + } + + return null; + } + + internal static string AsHex (this byte[] blob) + { + var hex = new StringBuilder (blob.Length * 2); + + for (int i = 0; i < blob.Length; i++) + hex.Append (blob[i].ToString ("x2")); + + return hex.ToString (); + } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// A fingerprint is a SHA-1 hash of the raw certificate data and is often used + /// as a unique identifier for a particular certificate in a certificate store. + /// + /// The fingerprint. + /// The certificate. + /// + /// is null. + /// + public static string GetFingerprint (this X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var encoded = certificate.GetEncoded (); + var fingerprint = new byte[20]; + var sha1 = new Sha1Digest (); + + sha1.BlockUpdate (encoded, 0, encoded.Length); + sha1.DoFinal (fingerprint, 0); + + return fingerprint.AsHex (); + } + + /// + /// Gets the public key algorithm for the certificate. + /// + /// + /// Gets the public key algorithm for the ceretificate. + /// + /// The public key algorithm. + /// The certificate. + /// + /// is null. + /// + public static PublicKeyAlgorithm GetPublicKeyAlgorithm (this X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var pubkey = certificate.GetPublicKey (); + + if (pubkey is DsaKeyParameters) + return PublicKeyAlgorithm.Dsa; + if (pubkey is RsaKeyParameters) + return PublicKeyAlgorithm.RsaGeneral; + if (pubkey is ElGamalKeyParameters) + return PublicKeyAlgorithm.ElGamalGeneral; + if (pubkey is ECKeyParameters) + return PublicKeyAlgorithm.EllipticCurve; + if (pubkey is DHKeyParameters) + return PublicKeyAlgorithm.DiffieHellman; + + return PublicKeyAlgorithm.None; + } + + internal static X509KeyUsageFlags GetKeyUsageFlags (bool[] usage) + { + var flags = X509KeyUsageFlags.None; + + if (usage == null || usage[(int) X509KeyUsageBits.DigitalSignature]) + flags |= X509KeyUsageFlags.DigitalSignature; + if (usage == null || usage[(int) X509KeyUsageBits.NonRepudiation]) + flags |= X509KeyUsageFlags.NonRepudiation; + if (usage == null || usage[(int) X509KeyUsageBits.KeyEncipherment]) + flags |= X509KeyUsageFlags.KeyEncipherment; + if (usage == null || usage[(int) X509KeyUsageBits.DataEncipherment]) + flags |= X509KeyUsageFlags.DataEncipherment; + if (usage == null || usage[(int) X509KeyUsageBits.KeyAgreement]) + flags |= X509KeyUsageFlags.KeyAgreement; + if (usage == null || usage[(int) X509KeyUsageBits.KeyCertSign]) + flags |= X509KeyUsageFlags.KeyCertSign; + if (usage == null || usage[(int) X509KeyUsageBits.CrlSign]) + flags |= X509KeyUsageFlags.CrlSign; + if (usage == null || usage[(int) X509KeyUsageBits.EncipherOnly]) + flags |= X509KeyUsageFlags.EncipherOnly; + if (usage == null || usage[(int) X509KeyUsageBits.DecipherOnly]) + flags |= X509KeyUsageFlags.DecipherOnly; + + return flags; + } + + /// + /// Gets the key usage flags. + /// + /// + /// The X.509 Key Usage Flags are used to determine which operations a certificate + /// may be used for. + /// + /// The key usage flags. + /// The certificate. + /// + /// is null. + /// + public static X509KeyUsageFlags GetKeyUsageFlags (this X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + return GetKeyUsageFlags (certificate.GetKeyUsage ()); + } + + static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (byte[] rawData) + { + using (var memory = new MemoryStream (rawData, false)) { + using (var asn1 = new Asn1InputStream (memory)) { + var algorithms = new List (); + var sequence = asn1.ReadObject () as Asn1Sequence; + + if (sequence == null) + return null; + + for (int i = 0; i < sequence.Count; i++) { + var identifier = AlgorithmIdentifier.GetInstance (sequence[i]); + EncryptionAlgorithm algorithm; + + if (BouncyCastleSecureMimeContext.TryGetEncryptionAlgorithm (identifier, out algorithm)) + algorithms.Add (algorithm); + } + + return algorithms.ToArray (); + } + } + } + + /// + /// Get the encryption algorithms that can be used with an X.509 certificate. + /// + /// + /// Scans the X.509 certificate for the S/MIME capabilities extension. If found, + /// the supported encryption algorithms will be decoded and returned. + /// If no extension can be found, the + /// algorithm is returned. + /// + /// The encryption algorithms. + /// The X.509 certificate. + /// + /// is null. + /// + public static EncryptionAlgorithm[] GetEncryptionAlgorithms (this X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var capabilities = certificate.GetExtensionValue (SmimeAttributes.SmimeCapabilities); + + if (capabilities != null) + return DecodeEncryptionAlgorithms (capabilities.GetOctets ()); + + return new EncryptionAlgorithm[] { EncryptionAlgorithm.TripleDes }; + } + + internal static bool IsDelta (this X509Crl crl) + { + var critical = crl.GetCriticalExtensionOids (); + + return critical != null ? critical.Contains (X509Extensions.DeltaCrlIndicator.Id) : false; + } + } +} diff --git a/src/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs b/src/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs new file mode 100644 index 0000000..616060f --- /dev/null +++ b/src/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs @@ -0,0 +1,1339 @@ +// +// BouncyCastleSecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +#if ENABLE_LDAP +using System.DirectoryServices.Protocols; +using SearchScope = System.DirectoryServices.Protocols.SearchScope; +#endif + +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Asn1.Cms; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.Smime; +using Org.BouncyCastle.X509.Store; +using Org.BouncyCastle.Utilities.Date; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities.Collections; + +using AttributeTable = Org.BouncyCastle.Asn1.Cms.AttributeTable; +using IssuerAndSerialNumber = Org.BouncyCastle.Asn1.Cms.IssuerAndSerialNumber; + +using MimeKit.IO; + +namespace MimeKit.Cryptography +{ + /// + /// A Secure MIME (S/MIME) cryptography context. + /// + /// + /// An abstract S/MIME context built around the BouncyCastle API. + /// + public abstract class BouncyCastleSecureMimeContext : SecureMimeContext + { + static readonly string RsassaPssOid = PkcsObjectIdentifiers.IdRsassaPss.Id; + + HttpClient client; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new + /// + protected BouncyCastleSecureMimeContext () + { + client = new HttpClient (); + } + + /// + /// Get or set whether or not certificate revocation lists should be downloaded when verifying signatures. + /// + /// + /// Gets or sets whether or not certificate revocation lists should be downloaded when verifying + /// signatures. + /// If enabled, the will attempt to automatically download + /// Certificate Revocation Lists (CRLs) from the internet based on the CRL Distribution Point extension on + /// each certificate. + /// Enabling this feature opens the client up to potential privacy risks. An attacker + /// can generate a custom X.509 certificate containing a CRL Distribution Point or OCSP URL pointing to an + /// attacker-controlled server, thereby getting a notification when the user decrypts the message or verifies + /// its digital signature. + /// + /// true if CRLs should be downloaded automatically; otherwise, false. + public bool CheckCertificateRevocation { + get; set; + } + + /// + /// Get the X.509 certificate matching the specified selector. + /// + /// + /// Gets the first certificate that matches the specified selector. + /// This method is used when constructing a certificate chain if the S/MIME + /// signature does not include a signer's certificate. + /// + /// The certificate on success; otherwise null. + /// The search criteria for the certificate. + protected abstract X509Certificate GetCertificate (IX509Selector selector); + + /// + /// Get the private key for the certificate matching the specified selector. + /// + /// + /// Gets the private key for the first certificate that matches the specified selector. + /// This method is used when signing or decrypting content. + /// + /// The private key on success; otherwise, null. + /// The search criteria for the private key. + protected abstract AsymmetricKeyParameter GetPrivateKey (IX509Selector selector); + + /// + /// Get the trusted anchors. + /// + /// + /// A trusted anchor is a trusted root-level X.509 certificate, + /// generally issued by a certificate authority (CA). + /// This method is used to build a certificate chain while verifying + /// signed content. + /// + /// The trusted anchors. + protected abstract HashSet GetTrustedAnchors (); + + /// + /// Get the intermediate certificates. + /// + /// + /// An intermediate certificate is any certificate that exists between the root + /// certificate issued by a Certificate Authority (CA) and the certificate at + /// the end of the chain. + /// This method is used to build a certificate chain while verifying + /// signed content. + /// + /// The intermediate certificates. + protected abstract IX509Store GetIntermediateCertificates (); + + /// + /// Get the certificate revocation lists. + /// + /// + /// A Certificate Revocation List (CRL) is a list of certificate serial numbers issued + /// by a particular Certificate Authority (CA) that have been revoked, either by the CA + /// itself or by the owner of the revoked certificate. + /// + /// The certificate revocation lists. + protected abstract IX509Store GetCertificateRevocationLists (); + + /// + /// Get the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// + /// Gets the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// The date & time for the next update (in UTC). + /// The issuer. + protected abstract DateTime GetNextCertificateRevocationListUpdate (X509Name issuer); + + /// + /// Get the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate certificate and + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address. + /// + /// A . + /// The mailbox. + /// + /// A certificate for the specified could not be found. + /// + protected abstract CmsRecipient GetCmsRecipient (MailboxAddress mailbox); + + /// + /// Get a collection of CmsRecipients for the specified mailboxes. + /// + /// + /// Gets a collection of CmsRecipients for the specified mailboxes. + /// + /// A . + /// The mailboxes. + /// + /// is null. + /// + /// + /// A certificate for one or more of the specified could not be found. + /// + protected CmsRecipientCollection GetCmsRecipients (IEnumerable mailboxes) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + var recipients = new CmsRecipientCollection (); + + foreach (var mailbox in mailboxes) + recipients.Add (GetCmsRecipient (mailbox)); + + return recipients; + } + + /// + /// Get the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate signing certificate + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address for database lookups. + /// + /// A . + /// The mailbox. + /// The preferred digest algorithm. + /// + /// A certificate for the specified could not be found. + /// + protected abstract CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo); + + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// This method is called when decoding digital signatures that include S/MIME capabilities in the metadata, allowing custom + /// implementations to update the X.509 certificate records with the list of preferred encryption algorithms specified by the + /// sending client. + /// + /// The certificate. + /// The encryption algorithm capabilities of the client (in preferred order). + /// The timestamp. + protected abstract void UpdateSecureMimeCapabilities (X509Certificate certificate, EncryptionAlgorithm[] algorithms, DateTime timestamp); + + CmsAttributeTableGenerator AddSecureMimeCapabilities (AttributeTable signedAttributes) + { + var attr = GetSecureMimeCapabilitiesAttribute (true); + + // populate our signed attributes with some S/MIME capabilities + return new DefaultSignedAttributeTableGenerator (signedAttributes.Add (attr.AttrType, attr.AttrValues[0])); + } + + Stream Sign (CmsSigner signer, Stream content, bool encapsulate) + { + var unsignedAttributes = new SimpleAttributeTableGenerator (signer.UnsignedAttributes); + var signedAttributes = AddSecureMimeCapabilities (signer.SignedAttributes); + var signedData = new CmsSignedDataStreamGenerator (); + var digestOid = GetDigestOid (signer.DigestAlgorithm); + byte[] subjectKeyId = null; + + if (signer.SignerIdentifierType == SubjectIdentifierType.SubjectKeyIdentifier) { + var subjectKeyIdentifier = signer.Certificate.GetExtensionValue (X509Extensions.SubjectKeyIdentifier); + if (subjectKeyIdentifier != null) { + var id = (Asn1OctetString) Asn1Object.FromByteArray (subjectKeyIdentifier.GetOctets ()); + subjectKeyId = id.GetOctets (); + } + } + + if (signer.PrivateKey is RsaKeyParameters && signer.RsaSignaturePadding == RsaSignaturePadding.Pss) { + if (subjectKeyId == null) + signedData.AddSigner (signer.PrivateKey, signer.Certificate, RsassaPssOid, digestOid, signedAttributes, unsignedAttributes); + else + signedData.AddSigner (signer.PrivateKey, subjectKeyId, RsassaPssOid, digestOid, signedAttributes, unsignedAttributes); + } else if (subjectKeyId == null) { + signedData.AddSigner (signer.PrivateKey, signer.Certificate, digestOid, signedAttributes, unsignedAttributes); + } else { + signedData.AddSigner (signer.PrivateKey, subjectKeyId, digestOid, signedAttributes, unsignedAttributes); + } + + signedData.AddCertificates (signer.CertificateChain); + + var memory = new MemoryBlockStream (); + + using (var stream = signedData.Open (memory, encapsulate)) + content.CopyTo (stream, 4096); + + memory.Position = 0; + + return memory; + } + + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime EncapsulatedSign (CmsSigner signer, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + return new ApplicationPkcs7Mime (SecureMimeType.SignedData, Sign (signer, content, true)); + } + + /// + /// Cryptographically signs and encapsulates the content using the specified signer and digest algorithm. + /// + /// + /// Cryptographically signs and encapsulates the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime EncapsulatedSign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var cmsSigner = GetCmsSigner (signer, digestAlgo); + + return EncapsulatedSign (cmsSigner, content); + } + + /// + /// Cryptographically signs the content using the specified signer. + /// + /// + /// Cryptographically signs the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Signature Sign (CmsSigner signer, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + return new ApplicationPkcs7Signature (Sign (signer, content, false)); + } + + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var cmsSigner = GetCmsSigner (signer, digestAlgo); + + return Sign (cmsSigner, content); + } + + X509Certificate GetCertificate (IX509Store store, SignerID signer) + { + var matches = store.GetMatches (signer); + + foreach (X509Certificate certificate in matches) + return certificate; + + return GetCertificate (signer); + } + + /// + /// Build a certificate chain. + /// + /// + /// Builds a certificate chain for the provided certificate to include when signing. + /// This method is ideal for use with custom + /// implementations when it is desirable to include the certificate chain + /// in the signature. + /// + /// The certificate to build the chain for. + /// The certificate chain, including the specified certificate. + protected IList BuildCertificateChain (X509Certificate certificate) + { + var selector = new X509CertStoreSelector (); + selector.Certificate = certificate; + + var intermediates = new X509CertificateStore (); + intermediates.Add (certificate); + + var parameters = new PkixBuilderParameters (GetTrustedAnchors (), selector); + parameters.ValidityModel = PkixParameters.PkixValidityModel; + parameters.AddStore (intermediates); + parameters.AddStore (GetIntermediateCertificates ()); + parameters.IsRevocationEnabled = false; + parameters.Date = new DateTimeObject (DateTime.UtcNow); + + var builder = new PkixCertPathBuilder (); + var result = builder.Build (parameters); + + var chain = new X509Certificate[result.CertPath.Certificates.Count]; + + for (int i = 0; i < chain.Length; i++) + chain[i] = (X509Certificate) result.CertPath.Certificates[i]; + + return chain; + } + + PkixCertPath BuildCertPath (HashSet anchors, IX509Store certificates, IX509Store crls, X509Certificate certificate, DateTime signingTime) + { + var selector = new X509CertStoreSelector (); + selector.Certificate = certificate; + + var intermediates = new X509CertificateStore (); + intermediates.Add (certificate); + + foreach (X509Certificate cert in certificates.GetMatches (null)) + intermediates.Add (cert); + + var parameters = new PkixBuilderParameters (anchors, selector); + parameters.AddStore (intermediates); + parameters.AddStore (crls); + + parameters.AddStore (GetIntermediateCertificates ()); + parameters.AddStore (GetCertificateRevocationLists ()); + + parameters.ValidityModel = PkixParameters.PkixValidityModel; + parameters.IsRevocationEnabled = false; + + if (signingTime != default (DateTime)) + parameters.Date = new DateTimeObject (signingTime); + + var builder = new PkixCertPathBuilder (); + var result = builder.Build (parameters); + + return result.CertPath; + } + + /// + /// Attempts to map a + /// to a . + /// + /// + /// Attempts to map a + /// to a . + /// + /// true if the algorithm identifier was successfully mapped; otherwise, false. + /// The algorithm identifier. + /// The encryption algorithm. + /// + /// is null. + /// + internal protected static bool TryGetDigestAlgorithm (AlgorithmIdentifier identifier, out DigestAlgorithm algorithm) + { + if (identifier == null) + throw new ArgumentNullException (nameof (identifier)); + + return TryGetDigestAlgorithm (identifier.Algorithm.Id, out algorithm); + } + + /// + /// Attempts to map a + /// to a . + /// + /// + /// Attempts to map a + /// to a . + /// + /// true if the algorithm identifier was successfully mapped; otherwise, false. + /// The algorithm identifier. + /// The encryption algorithm. + /// + /// is null. + /// + internal protected static bool TryGetEncryptionAlgorithm (AlgorithmIdentifier identifier, out EncryptionAlgorithm algorithm) + { + if (identifier == null) + throw new ArgumentNullException (nameof (identifier)); + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Aes256Cbc) { + algorithm = EncryptionAlgorithm.Aes256; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Aes192Cbc) { + algorithm = EncryptionAlgorithm.Aes192; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Aes128Cbc) { + algorithm = EncryptionAlgorithm.Aes128; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Camellia256Cbc) { + algorithm = EncryptionAlgorithm.Camellia256; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Camellia192Cbc) { + algorithm = EncryptionAlgorithm.Camellia192; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Camellia128Cbc) { + algorithm = EncryptionAlgorithm.Camellia128; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.Cast5Cbc) { + algorithm = EncryptionAlgorithm.Cast5; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.DesEde3Cbc) { + algorithm = EncryptionAlgorithm.TripleDes; + return true; + } + + if (identifier.Algorithm.Id == Blowfish.Id) { + algorithm = EncryptionAlgorithm.Blowfish; + return true; + } + + if (identifier.Algorithm.Id == Twofish.Id) { + algorithm = EncryptionAlgorithm.Twofish; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.SeedCbc) { + algorithm = EncryptionAlgorithm.Seed; + return true; + } + + if (identifier.Algorithm.Id == SmimeCapability.DesCbc.Id) { + algorithm = EncryptionAlgorithm.Des; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.IdeaCbc) { + algorithm = EncryptionAlgorithm.Idea; + return true; + } + + if (identifier.Algorithm.Id == CmsEnvelopedGenerator.RC2Cbc) { + if (identifier.Parameters is DerSequence) { + var param = (DerSequence) identifier.Parameters; + var version = (DerInteger) param[0]; + int bits = version.Value.IntValue; + + switch (bits) { + case 58: algorithm = EncryptionAlgorithm.RC2128; return true; + case 120: algorithm = EncryptionAlgorithm.RC264; return true; + case 160: algorithm = EncryptionAlgorithm.RC240; return true; + } + } else { + var param = (DerInteger) identifier.Parameters; + int bits = param.Value.IntValue; + + switch (bits) { + case 128: algorithm = EncryptionAlgorithm.RC2128; return true; + case 64: algorithm = EncryptionAlgorithm.RC264; return true; + case 40: algorithm = EncryptionAlgorithm.RC240; return true; + } + } + } + + algorithm = EncryptionAlgorithm.RC240; + + return false; + } + + async Task DownloadCrlsOverHttpAsync (string location, Stream stream, bool doAsync, CancellationToken cancellationToken) + { + try { + if (doAsync) { + using (var response = await client.GetAsync (location, cancellationToken).ConfigureAwait (false)) + await response.Content.CopyToAsync (stream).ConfigureAwait (false); + } else { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + cancellationToken.ThrowIfCancellationRequested (); + + var request = (HttpWebRequest) WebRequest.Create (location); + using (var response = request.GetResponse ()) { + var content = response.GetResponseStream (); + content.CopyTo (stream, 4096); + } +#else + using (var response = client.GetAsync (location, cancellationToken).GetAwaiter ().GetResult ()) + response.Content.CopyToAsync (stream).GetAwaiter ().GetResult (); +#endif + } + + return true; + } catch { + return false; + } + } + +#if ENABLE_LDAP + // https://msdn.microsoft.com/en-us/library/bb332056.aspx#sdspintro_topic3_lpadconn + bool DownloadCrlsOverLdap (string location, Stream stream, CancellationToken cancellationToken) + { + LdapUri uri; + + cancellationToken.ThrowIfCancellationRequested (); + + if (!LdapUri.TryParse (location, out uri) || string.IsNullOrEmpty (uri.Host) || string.IsNullOrEmpty (uri.DistinguishedName)) + return false; + + try { + // Note: Mono doesn't support this... + LdapDirectoryIdentifier identifier; + + if (uri.Port > 0) + identifier = new LdapDirectoryIdentifier (uri.Host, uri.Port, false, true); + else + identifier = new LdapDirectoryIdentifier (uri.Host, false, true); + + using (var ldap = new LdapConnection (identifier)) { + if (uri.Scheme.Equals ("ldaps", StringComparison.OrdinalIgnoreCase)) + ldap.SessionOptions.SecureSocketLayer = true; + + ldap.Bind (); + + var request = new SearchRequest (uri.DistinguishedName, uri.Filter, uri.Scope, uri.Attributes); + var response = (SearchResponse) ldap.SendRequest (request); + + foreach (SearchResultEntry entry in response.Entries) { + foreach (DirectoryAttribute attribute in entry.Attributes) { + var values = attribute.GetValues (typeof (byte[])); + + for (int i = 0; i < values.Length; i++) { + var buffer = (byte[]) values[i]; + + stream.Write (buffer, 0, buffer.Length); + } + } + } + } + + return true; + } catch { + return false; + } + } +#endif + + async Task DownloadCrlsAsync (X509Certificate certificate, bool doAsync, CancellationToken cancellationToken) + { + var nextUpdate = GetNextCertificateRevocationListUpdate (certificate.IssuerDN); + var now = DateTime.UtcNow; + Asn1OctetString cdp; + + if (nextUpdate > now) + return; + + if ((cdp = certificate.GetExtensionValue (X509Extensions.CrlDistributionPoints)) == null) + return; + + var asn1 = Asn1Object.FromByteArray (cdp.GetOctets ()); + var crlDistributionPoint = CrlDistPoint.GetInstance (asn1); + var points = crlDistributionPoint.GetDistributionPoints (); + + using (var stream = new MemoryBlockStream ()) { +#if ENABLE_LDAP + var ldapLocations = new List (); +#endif + bool downloaded = false; + + for (int i = 0; i < points.Length; i++) { + var generalNames = GeneralNames.GetInstance (points[i].DistributionPointName.Name).GetNames (); + for (int j = 0; j < generalNames.Length && !downloaded; j++) { + if (generalNames[j].TagNo != GeneralName.UniformResourceIdentifier) + continue; + + var location = DerIA5String.GetInstance (generalNames[j].Name).GetString (); + var colon = location.IndexOf (':'); + + if (colon == -1) + continue; + + var protocol = location.Substring (0, colon).ToLowerInvariant (); + + switch (protocol) { + case "https": case "http": + downloaded = await DownloadCrlsOverHttpAsync (location, stream, doAsync, cancellationToken).ConfigureAwait (false); + break; +#if ENABLE_LDAP + case "ldaps": case "ldap": + // Note: delay downloading from LDAP urls in case we find an HTTP url instead since LDAP + // won't be as reliable on Mono systems which do not implement the LDAP functionality. + ldapLocations.Add (location); + break; +#endif + } + } + } + +#if ENABLE_LDAP + for (int i = 0; i < ldapLocations.Count && !downloaded; i++) + downloaded = DownloadCrlsOverLdap (ldapLocations[i], stream, cancellationToken); +#endif + + if (!downloaded) + return; + + stream.Position = 0; + + var parser = new X509CrlParser (); + foreach (X509Crl crl in parser.ReadCrls (stream)) + Import (crl); + } + } + + /// + /// Get the list of digital signatures. + /// + /// + /// Gets the list of digital signatures. + /// This method is useful to call from within any custom + /// Verify + /// method that you may implement in your own class. + /// + /// The digital signatures. + /// The CMS signed data parser. + /// Whether or not the operation should be done asynchronously. + /// The cancellation token. + async Task GetDigitalSignaturesAsync (CmsSignedDataParser parser, bool doAsync, CancellationToken cancellationToken) + { + var certificates = parser.GetCertificates ("Collection"); + var signatures = new List (); + var crls = parser.GetCrls ("Collection"); + var store = parser.GetSignerInfos (); + + foreach (SignerInformation signerInfo in store.GetSigners ()) { + var certificate = GetCertificate (certificates, signerInfo.SignerID); + var signature = new SecureMimeDigitalSignature (signerInfo, certificate); + + if (CheckCertificateRevocation && certificate != null) + await DownloadCrlsAsync (certificate, doAsync, cancellationToken).ConfigureAwait (false); + + if (certificate != null) { + Import (certificate); + + if (signature.EncryptionAlgorithms.Length > 0 && signature.CreationDate != default (DateTime)) + UpdateSecureMimeCapabilities (certificate, signature.EncryptionAlgorithms, signature.CreationDate); + } + + var anchors = GetTrustedAnchors (); + + try { + signature.Chain = BuildCertPath (anchors, certificates, crls, certificate, signature.CreationDate); + } catch (Exception ex) { + signature.ChainException = ex; + } + + signatures.Add (signature); + } + + return new DigitalSignatureCollection (signatures); + } + + /// + /// Verify the specified content using the detached signature data. + /// + /// + /// Verifies the specified content using the detached signature data. + /// + /// A list of the digital signatures. + /// The content. + /// The detached signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + var parser = new CmsSignedDataParser (new CmsTypedStream (content), signatureData); + var signed = parser.GetSignedContent (); + + signed.Drain (); + + return GetDigitalSignaturesAsync (parser, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the specified content using the detached signature data. + /// + /// + /// Verifies the specified content using the detached signature data. + /// + /// A list of the digital signatures. + /// The content. + /// The detached signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + var parser = new CmsSignedDataParser (new CmsTypedStream (content), signatureData); + var signed = parser.GetSignedContent (); + + signed.Drain (); + + return GetDigitalSignaturesAsync (parser, true, cancellationToken); + } + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The list of digital signatures. + /// The signed data. + /// The extracted MIME entity. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The extracted content could not be parsed as a MIME entity. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override DigitalSignatureCollection Verify (Stream signedData, out MimeEntity entity, CancellationToken cancellationToken = default (CancellationToken)) + { + if (signedData == null) + throw new ArgumentNullException (nameof (signedData)); + + var parser = new CmsSignedDataParser (signedData); + var signed = parser.GetSignedContent (); + + entity = MimeEntity.Load (signed.ContentStream, cancellationToken); + signed.Drain (); + + return GetDigitalSignaturesAsync (parser, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The extracted content stream. + /// The signed data. + /// The digital signatures. + /// The cancellation token. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override Stream Verify (Stream signedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + if (signedData == null) + throw new ArgumentNullException (nameof (signedData)); + + var parser = new CmsSignedDataParser (signedData); + var signed = parser.GetSignedContent (); + var content = new MemoryBlockStream (); + + signed.ContentStream.CopyTo (content, 4096); + content.Position = 0; + signed.Drain (); + + signatures = GetDigitalSignaturesAsync (parser, false, cancellationToken).GetAwaiter ().GetResult (); + + return content; + } + + class CmsRecipientInfoGenerator : RecipientInfoGenerator + { + readonly CmsRecipient recipient; + + public CmsRecipientInfoGenerator (CmsRecipient recipient) + { + this.recipient = recipient; + } + + IWrapper CreateWrapper (AlgorithmIdentifier keyExchangeAlgorithm) + { + string name; + + if (PkcsObjectIdentifiers.IdRsaesOaep.Id.Equals (keyExchangeAlgorithm.Algorithm.Id, StringComparison.Ordinal)) { + var oaepParameters = RsaesOaepParameters.GetInstance (keyExchangeAlgorithm.Parameters); + name = "RSA//OAEPWITH" + DigestUtilities.GetAlgorithmName (oaepParameters.HashAlgorithm.Algorithm) + "ANDMGF1Padding"; + } else if (PkcsObjectIdentifiers.RsaEncryption.Id.Equals (keyExchangeAlgorithm.Algorithm.Id, StringComparison.Ordinal)) { + name = "RSA//PKCS1Padding"; + } else { + name = keyExchangeAlgorithm.Algorithm.Id; + } + + return WrapperUtilities.GetWrapper (name); + } + + byte[] GenerateWrappedKey (KeyParameter contentEncryptionKey, AlgorithmIdentifier keyEncryptionAlgorithm, AsymmetricKeyParameter publicKey, SecureRandom random) + { + var keyWrapper = CreateWrapper (keyEncryptionAlgorithm); + var keyBytes = contentEncryptionKey.GetKey (); + + keyWrapper.Init (true, new ParametersWithRandom (publicKey, random)); + + return keyWrapper.Wrap (keyBytes, 0, keyBytes.Length); + } + + public RecipientInfo Generate (KeyParameter contentEncryptionKey, SecureRandom random) + { + var tbs = Asn1Object.FromByteArray (recipient.Certificate.GetTbsCertificate ()); + var certificate = TbsCertificateStructure.GetInstance (tbs); + var publicKey = recipient.Certificate.GetPublicKey (); + var publicKeyInfo = certificate.SubjectPublicKeyInfo; + AlgorithmIdentifier keyEncryptionAlgorithm; + + if (publicKey is RsaKeyParameters && recipient.RsaEncryptionPadding?.Scheme == RsaEncryptionPaddingScheme.Oaep) { + keyEncryptionAlgorithm = recipient.RsaEncryptionPadding.GetAlgorithmIdentifier (); + } else { + keyEncryptionAlgorithm = publicKeyInfo.AlgorithmID; + } + + var encryptedKeyBytes = GenerateWrappedKey (contentEncryptionKey, keyEncryptionAlgorithm, publicKey, random); + RecipientIdentifier recipientIdentifier = null; + + if (recipient.RecipientIdentifierType == SubjectIdentifierType.SubjectKeyIdentifier) { + var subjectKeyIdentifier = recipient.Certificate.GetExtensionValue (X509Extensions.SubjectKeyIdentifier); + recipientIdentifier = new RecipientIdentifier (subjectKeyIdentifier); + } + + if (recipientIdentifier == null) { + var issuerAndSerial = new IssuerAndSerialNumber (certificate.Issuer, certificate.SerialNumber.Value); + recipientIdentifier = new RecipientIdentifier (issuerAndSerial); + } + + return new RecipientInfo (new KeyTransRecipientInfo (recipientIdentifier, keyEncryptionAlgorithm, + new DerOctetString (encryptedKeyBytes))); + } + } + + Stream Envelope (CmsRecipientCollection recipients, Stream content) + { + var unique = new HashSet (); + var cms = new CmsEnvelopedDataGenerator (); + int count = 0; + + foreach (var recipient in recipients) { + if (unique.Add (recipient.Certificate)) { + cms.AddRecipientInfoGenerator (new CmsRecipientInfoGenerator (recipient)); + count++; + } + } + + if (count == 0) + throw new ArgumentException ("No recipients specified.", nameof (recipients)); + + var algorithm = GetPreferredEncryptionAlgorithm (recipients); + var input = new CmsProcessableInputStream (content); + CmsEnvelopedData envelopedData; + + switch (algorithm) { + case EncryptionAlgorithm.Aes128: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Aes128Cbc); + break; + case EncryptionAlgorithm.Aes192: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Aes192Cbc); + break; + case EncryptionAlgorithm.Aes256: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Aes256Cbc); + break; + case EncryptionAlgorithm.Blowfish: + envelopedData = cms.Generate (input, Blowfish.Id); + break; + case EncryptionAlgorithm.Camellia128: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Camellia128Cbc); + break; + case EncryptionAlgorithm.Camellia192: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Camellia192Cbc); + break; + case EncryptionAlgorithm.Camellia256: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Camellia256Cbc); + break; + case EncryptionAlgorithm.Cast5: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.Cast5Cbc); + break; + case EncryptionAlgorithm.Des: + envelopedData = cms.Generate (input, SmimeCapability.DesCbc.Id); + break; + case EncryptionAlgorithm.Idea: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.IdeaCbc); + break; + case EncryptionAlgorithm.RC240: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.RC2Cbc, 40); + break; + case EncryptionAlgorithm.RC264: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.RC2Cbc, 64); + break; + case EncryptionAlgorithm.RC2128: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.RC2Cbc, 128); + break; + case EncryptionAlgorithm.Seed: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.SeedCbc); + break; + case EncryptionAlgorithm.TripleDes: + envelopedData = cms.Generate (input, CmsEnvelopedGenerator.DesEde3Cbc); + break; + //case EncryptionAlgorithm.Twofish: + // envelopedData = cms.Generate (input, Twofish.Id); + // break; + default: + throw new NotSupportedException (string.Format ("The {0} encryption algorithm is not supported by the {1}.", algorithm, GetType ().Name)); + } + + return new MemoryStream (envelopedData.GetEncoded (), false); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted content. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime Encrypt (CmsRecipientCollection recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + return new ApplicationPkcs7Mime (SecureMimeType.EnvelopedData, Envelope (recipients, content)); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A certificate for one or more of the could not be found. + /// + /// + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Encrypt (IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + return Encrypt (GetCmsRecipients (recipients), content); + } + + /// + /// Decrypt the specified encryptedData. + /// + /// + /// Decrypts the specified encryptedData. + /// + /// The decrypted . + /// The encrypted data. + /// The cancellation token. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + var parser = new CmsEnvelopedDataParser (encryptedData); + var recipients = parser.GetRecipientInfos (); + var algorithm = parser.EncryptionAlgorithmID; + AsymmetricKeyParameter key; + + foreach (RecipientInformation recipient in recipients.GetRecipients ()) { + if ((key = GetPrivateKey (recipient.RecipientID)) == null) + continue; + + var content = recipient.GetContent (key); + var memory = new MemoryStream (content, false); + + return MimeEntity.Load (memory, true, cancellationToken); + } + + throw new CmsException ("A suitable private key could not be found for decrypting."); + } + + /// + /// Decrypt the specified encryptedData to an output stream. + /// + /// + /// Decrypts the specified encryptedData to an output stream. + /// + /// The encrypted data. + /// The output stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override void DecryptTo (Stream encryptedData, Stream decryptedData) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + if (decryptedData == null) + throw new ArgumentNullException (nameof (decryptedData)); + + var parser = new CmsEnvelopedDataParser (encryptedData); + var recipients = parser.GetRecipientInfos (); + var algorithm = parser.EncryptionAlgorithmID; + AsymmetricKeyParameter key; + + foreach (RecipientInformation recipient in recipients.GetRecipients ()) { + if ((key = GetPrivateKey (recipient.RecipientID)) == null) + continue; + + var content = recipient.GetContentStream (key); + content.ContentStream.CopyTo (decryptedData, 4096); + return; + } + + throw new CmsException ("A suitable private key could not be found for decrypting."); + } + + /// + /// Export the certificates for the specified mailboxes. + /// + /// + /// Exports the certificates for the specified mailboxes. + /// + /// A new instance containing + /// the exported keys. + /// The mailboxes. + /// + /// is null. + /// + /// + /// No mailboxes were specified. + /// + /// + /// A certificate for one or more of the could not be found. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Export (IEnumerable mailboxes) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + var certificates = new X509CertificateStore (); + int count = 0; + + foreach (var mailbox in mailboxes) { + var recipient = GetCmsRecipient (mailbox); + certificates.Add (recipient.Certificate); + count++; + } + + if (count == 0) + throw new ArgumentException ("No mailboxes specified.", nameof (mailboxes)); + + var cms = new CmsSignedDataStreamGenerator (); + cms.AddCertificates (certificates); + + var memory = new MemoryBlockStream (); + cms.Open (memory).Dispose (); + memory.Position = 0; + + return new ApplicationPkcs7Mime (SecureMimeType.CertsOnly, memory); + } + + /// + /// Releases all resources used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + protected override void Dispose (bool disposing) + { + if (disposing && client != null) { + client.Dispose (); + client = null; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/CertificateNotFoundException.cs b/src/MimeKit/Cryptography/CertificateNotFoundException.cs new file mode 100644 index 0000000..1d538e7 --- /dev/null +++ b/src/MimeKit/Cryptography/CertificateNotFoundException.cs @@ -0,0 +1,115 @@ +// +// CertificateNotFoundException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MimeKit.Cryptography { + /// + /// An exception that is thrown when a certificate could not be found for a specified mailbox. + /// + /// + /// An exception that is thrown when a certificate could not be found for a specified mailbox. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class CertificateNotFoundException : Exception + { +#if SERIALIZABLE + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The stream context. + /// + /// is null. + /// + protected CertificateNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) + { + var text = info.GetString ("Mailbox"); + MailboxAddress mailbox; + + if (MailboxAddress.TryParse (text, out mailbox)) + Mailbox = mailbox; + } +#endif + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The mailbox that could not be resolved to a valid certificate. + /// A message explaining the error. + public CertificateNotFoundException (MailboxAddress mailbox, string message) : base (message) + { + Mailbox = mailbox; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("Mailbox", Mailbox.ToString (true)); + } +#endif + + /// + /// Gets the mailbox address that could not be resolved to a certificate. + /// + /// + /// Gets the mailbox address that could not be resolved to a certificate. + /// + /// The mailbox address. + public MailboxAddress Mailbox { + get; private set; + } + } +} diff --git a/src/MimeKit/Cryptography/CmsRecipient.cs b/src/MimeKit/Cryptography/CmsRecipient.cs new file mode 100644 index 0000000..aae14c1 --- /dev/null +++ b/src/MimeKit/Cryptography/CmsRecipient.cs @@ -0,0 +1,256 @@ +// +// CmsRecipient.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +using Org.BouncyCastle.X509; + +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME recipient. + /// + /// + /// If the X.509 certificates are known for each of the recipients, you + /// may wish to use a as opposed to having + /// the do its own certificate + /// lookups for each . + /// + public class CmsRecipient + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new based on the provided certificate. + /// If the X.509 certificate contains an S/MIME capability extension, the initial value of the + /// property will be set to whatever encryption algorithms are + /// defined by the S/MIME capability extension, otherwise int will be initialized to a list + /// containing only the Triple-Des encryption algorithm which should be safe to assume for all + /// modern S/MIME v3.x client implementations. + /// + /// The recipient's certificate. + /// The recipient identifier type. + /// + /// is null. + /// + public CmsRecipient (X509Certificate certificate, SubjectIdentifierType recipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (recipientIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + RecipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + RecipientIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + EncryptionAlgorithms = certificate.GetEncryptionAlgorithms (); + Certificate = certificate; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new , loading the certificate from the specified stream. + /// If the X.509 certificate contains an S/MIME capability extension, the initial value of the + /// property will be set to whatever encryption algorithms are + /// defined by the S/MIME capability extension, otherwise int will be initialized to a list + /// containing only the Triple-Des encryption algorithm which should be safe to assume for all + /// modern S/MIME v3.x client implementations. + /// + /// The stream containing the recipient's certificate. + /// The recipient identifier type. + /// + /// is null. + /// + /// + /// The specified file does not contain a certificate. + /// + /// + /// An I/O error occurred. + /// + public CmsRecipient (Stream stream, SubjectIdentifierType recipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (recipientIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + RecipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + RecipientIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + var parser = new X509CertificateParser (); + + Certificate = parser.ReadCertificate (stream); + + if (Certificate == null) + throw new FormatException (); + + EncryptionAlgorithms = Certificate.GetEncryptionAlgorithms (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new , loading the certificate from the specified file. + /// If the X.509 certificate contains an S/MIME capability extension, the initial value of the + /// property will be set to whatever encryption algorithms are + /// defined by the S/MIME capability extension, otherwise int will be initialized to a list + /// containing only the Triple-Des encryption algorithm which should be safe to assume for all + /// modern S/MIME v3.x client implementations. + /// + /// The file containing the recipient's certificate. + /// The recipient identifier type. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The specified file does not contain a certificate. + /// + /// + /// An I/O error occurred. + /// + public CmsRecipient (string fileName, SubjectIdentifierType recipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (recipientIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + RecipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + RecipientIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + using (var stream = File.OpenRead (fileName)) { + var parser = new X509CertificateParser (); + + Certificate = parser.ReadCertificate (stream); + } + + if (Certificate == null) + throw new FormatException (); + + EncryptionAlgorithms = Certificate.GetEncryptionAlgorithms (); + } + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new based on the provided certificate. + /// If the X.509 certificate contains an S/MIME capability extension, the initial value of the + /// property will be set to whatever encryption algorithms are + /// defined by the S/MIME capability extension, otherwise int will be initialized to a list + /// containing only the Triple-Des encryption algorithm which should be safe to assume for all + /// modern S/MIME v3.x client implementations. + /// + /// The recipient's certificate. + /// The recipient identifier type. + /// + /// is null. + /// + public CmsRecipient (X509Certificate2 certificate, SubjectIdentifierType recipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (recipientIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + RecipientIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + RecipientIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + EncryptionAlgorithms = certificate.GetEncryptionAlgorithms (); + Certificate = certificate.AsBouncyCastleCertificate (); + } +#endif + + /// + /// Gets the recipient's certificate. + /// + /// + /// The certificate is used for the purpose of encrypting data. + /// + /// The certificate. + public X509Certificate Certificate { + get; private set; + } + + /// + /// Gets the recipient identifier type. + /// + /// + /// Specifies how the certificate should be looked up on the recipient's end. + /// + /// The recipient identifier type. + public SubjectIdentifierType RecipientIdentifierType { + get; private set; + } + + /// + /// Gets or sets the known S/MIME encryption capabilities of the + /// recipient's mail client, in their preferred order. + /// + /// + /// Provides the with an array of + /// encryption algorithms that are known to be supported by the + /// recpipient's client software and should be in the recipient's + /// order of preference. + /// + /// The encryption algorithms. + public EncryptionAlgorithm[] EncryptionAlgorithms { + get; set; + } + + /// + /// Get or set the RSA key encryption padding. + /// + /// + /// Gets or sets the padding to use for key encryption when + /// the 's public key is an RSA key. + /// + /// The encryption padding scheme. + public RsaEncryptionPadding RsaEncryptionPadding { + get; set; + } + } +} diff --git a/src/MimeKit/Cryptography/CmsRecipientCollection.cs b/src/MimeKit/Cryptography/CmsRecipientCollection.cs new file mode 100644 index 0000000..e50234c --- /dev/null +++ b/src/MimeKit/Cryptography/CmsRecipientCollection.cs @@ -0,0 +1,208 @@ +// +// CmsRecipientCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MimeKit.Cryptography { + /// + /// A collection of objects. + /// + /// + /// If the X.509 certificates are known for each of the recipients, you + /// may wish to use a as opposed to + /// using the methods that take a list of + /// objects. + /// + public class CmsRecipientCollection : ICollection + { + readonly IList recipients; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public CmsRecipientCollection () + { + recipients = new List (); + } + + #region ICollection implementation + + /// + /// Gets the number of recipients in the collection. + /// + /// + /// Indicates the number of recipients in the collection. + /// + /// The number of recipients in the collection. + public int Count { + get { return recipients.Count; } + } + + /// + /// Get a value indicating whether the is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Adds the specified recipient. + /// + /// + /// Adds the specified recipient. + /// + /// The recipient. + /// + /// is null. + /// + public void Add (CmsRecipient recipient) + { + if (recipient == null) + throw new ArgumentNullException (nameof (recipient)); + + recipients.Add (recipient); + } + + /// + /// Clears the recipient collection. + /// + /// + /// Removes all of the recipients from the collection. + /// + public void Clear () + { + recipients.Clear (); + } + + /// + /// Checks if the collection contains the specified recipient. + /// + /// + /// Determines whether or not the collection contains the specified recipient. + /// + /// true if the specified recipient exists; + /// otherwise false. + /// The recipient. + /// + /// is null. + /// + public bool Contains (CmsRecipient recipient) + { + if (recipient == null) + throw new ArgumentNullException (nameof (recipient)); + + return recipients.Contains (recipient); + } + + /// + /// Copies all of the recipients in the to the specified array. + /// + /// + /// Copies all of the recipients within the into the array, + /// starting at the specified array index. + /// + /// The array. + /// The array index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (CmsRecipient[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException (nameof (array)); + + if (arrayIndex < 0 || arrayIndex + Count > array.Length) + throw new ArgumentOutOfRangeException (nameof (arrayIndex)); + + recipients.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified recipient. + /// + /// + /// Removes the specified recipient. + /// + /// true if the recipient was removed; otherwise false. + /// The recipient. + /// + /// is null. + /// + public bool Remove (CmsRecipient recipient) + { + if (recipient == null) + throw new ArgumentNullException (nameof (recipient)); + + return recipients.Remove (recipient); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the collection of recipients. + /// + /// + /// Gets an enumerator for the collection of recipients. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return recipients.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the collection of recipients. + /// + /// + /// Gets an enumerator for the collection of recipients. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return recipients.GetEnumerator (); + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/CmsSigner.cs b/src/MimeKit/Cryptography/CmsSigner.cs new file mode 100644 index 0000000..47c8ee8 --- /dev/null +++ b/src/MimeKit/Cryptography/CmsSigner.cs @@ -0,0 +1,464 @@ +// +// CmsSigner.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Asn1.Cms; + +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME signer. + /// + /// + /// If the X.509 certificate is known for the signer, you may wish to use a + /// as opposed to having the + /// do its own certificate lookup for the signer's . + /// + public class CmsSigner + { + /// + /// Initialize a new instance of the class. + /// + /// + /// The initial value of the will be set to + /// and both the + /// and properties + /// will be initialized to empty tables. + /// + CmsSigner () + { + UnsignedAttributes = new AttributeTable (new Dictionary ()); + SignedAttributes = new AttributeTable (new Dictionary ()); + DigestAlgorithm = DigestAlgorithm.Sha256; + } + + static bool CanSign (X509Certificate certificate) + { + var flags = certificate.GetKeyUsageFlags (); + + if (flags != X509KeyUsageFlags.None && (flags & SecureMimeContext.DigitalSignatureKeyUsageFlags) == 0) + return false; + + return true; + } + + static void CheckCertificateCanBeUsedForSigning (X509Certificate certificate) + { + if (!CanSign (certificate)) + throw new ArgumentException ("The certificate cannot be used for signing.", nameof (certificate)); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// The initial value of the will be set to + /// and both the + /// and properties + /// will be initialized to empty tables. + /// + /// The chain of certificates starting with the signer's certificate back to the root. + /// The signer's private key. + /// The scheme used for identifying the signer certificate. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// did not contain any certificates. + /// -or- + /// The certificate cannot be used for signing. + /// -or- + /// is not a private key. + /// + public CmsSigner (IEnumerable chain, AsymmetricKeyParameter key, SubjectIdentifierType signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) : this () + { + if (chain == null) + throw new ArgumentNullException (nameof (chain)); + + if (key == null) + throw new ArgumentNullException (nameof (key)); + + CertificateChain = new X509CertificateChain (chain); + + if (CertificateChain.Count == 0) + throw new ArgumentException ("The certificate chain was empty.", nameof (chain)); + + CheckCertificateCanBeUsedForSigning (CertificateChain[0]); + + if (!key.IsPrivate) + throw new ArgumentException ("The key must be a private key.", nameof (key)); + + if (signerIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + SignerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + SignerIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + Certificate = CertificateChain[0]; + PrivateKey = key; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// The initial value of the will + /// be set to and both the + /// and properties will be + /// initialized to empty tables. + /// + /// The signer's certificate. + /// The signer's private key. + /// The scheme used for identifying the signer certificate. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// is not a private key. + /// + public CmsSigner (X509Certificate certificate, AsymmetricKeyParameter key, SubjectIdentifierType signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) : this () + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + CheckCertificateCanBeUsedForSigning (certificate); + + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (!key.IsPrivate) + throw new ArgumentException ("The key must be a private key.", nameof (key)); + + if (signerIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + SignerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + SignerIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + CertificateChain = new X509CertificateChain (); + CertificateChain.Add (certificate); + Certificate = certificate; + PrivateKey = key; + } + + void LoadPkcs12 (Stream stream, string password, SubjectIdentifierType signerIdentifierType) + { + var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ()); + bool hasPrivateKey = false; + + foreach (string alias in pkcs12.Aliases) { + if (!pkcs12.IsKeyEntry (alias)) + continue; + + var chain = pkcs12.GetCertificateChain (alias); + var key = pkcs12.GetKey (alias); + + if (!key.Key.IsPrivate) + continue; + + hasPrivateKey = true; + + if (chain.Length == 0 || !CanSign (chain[0].Certificate)) + continue; + + if (signerIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + SignerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + SignerIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + CertificateChain = new X509CertificateChain (); + Certificate = chain[0].Certificate; + PrivateKey = key.Key; + + foreach (var entry in chain) + CertificateChain.Add (entry.Certificate); + + return; + } + + if (!hasPrivateKey) + throw new ArgumentException ("The stream did not contain a private key.", nameof (stream)); + + throw new ArgumentException ("The stream did not contain a certificate that could be used to create digital signatures.", nameof (stream)); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new , loading the X.509 certificate and private key + /// from the specified stream. + /// The initial value of the will + /// be set to and both the + /// and properties will be + /// initialized to empty tables. + /// + /// The raw certificate and key data in pkcs12 format. + /// The password to unlock the stream. + /// The scheme used for identifying the signer certificate. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain a private key. + /// -or- + /// does not contain a certificate that could be used for signing. + /// + /// + /// An I/O error occurred. + /// + public CmsSigner (Stream stream, string password, SubjectIdentifierType signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) : this () + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + LoadPkcs12 (stream, password, signerIdentifierType); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new , loading the X.509 certificate and private key + /// from the specified file. + /// The initial value of the will + /// be set to and both the + /// and properties will be + /// initialized to empty tables. + /// + /// The raw certificate and key data in pkcs12 format. + /// The password to unlock the stream. + /// The scheme used for identifying the signer certificate. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// -or- + /// does not contain a private key. + /// -or- + /// does not contain a certificate that could be used for signing. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + public CmsSigner (string fileName, string password, SubjectIdentifierType signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) : this () + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + using (var stream = File.OpenRead (fileName)) + LoadPkcs12 (stream, password, signerIdentifierType); + } + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + /// + /// Initialize a new instance of the class. + /// + /// + /// The initial value of the will + /// be set to and both the + /// and properties will be + /// initialized to empty tables. + /// + /// The signer's certificate. + /// The scheme used for identifying the signer certificate. + /// + /// is null. + /// + /// + /// cannot be used for signing. + /// + public CmsSigner (X509Certificate2 certificate, SubjectIdentifierType signerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber) : this () + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (!certificate.HasPrivateKey) + throw new ArgumentException ("The certificate does not contain a private key.", nameof (certificate)); + + var cert = certificate.AsBouncyCastleCertificate (); + var key = certificate.PrivateKey.AsAsymmetricKeyParameter (); + + CheckCertificateCanBeUsedForSigning (cert); + + if (signerIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + SignerIdentifierType = SubjectIdentifierType.IssuerAndSerialNumber; + else + SignerIdentifierType = SubjectIdentifierType.SubjectKeyIdentifier; + + CertificateChain = new X509CertificateChain (); + CertificateChain.Add (cert); + Certificate = cert; + PrivateKey = key; + } +#endif + + /// + /// Get the signer's certificate. + /// + /// + /// The signer's certificate that contains a public key that can be used for + /// verifying the digital signature. + /// + /// The signer's certificate. + public X509Certificate Certificate { + get; private set; + } + + /// + /// Get the certificate chain. + /// + /// + /// Gets the certificate chain. + /// + /// The certificate chain. + public X509CertificateChain CertificateChain { + get; private set; + } + + /// + /// Get or set the digest algorithm. + /// + /// + /// Specifies which digest algorithm to use to generate the + /// cryptographic hash of the content being signed. + /// + /// The digest algorithm. + public DigestAlgorithm DigestAlgorithm { + get; set; + } + + /// + /// Get the private key. + /// + /// + /// The private key used for signing. + /// + /// The private key. + public AsymmetricKeyParameter PrivateKey { + get; private set; + } + + /// + /// Get or set the RSA signature padding scheme. + /// + /// + /// Gets or sets the signature padding scheme to use for signing when + /// the is an RSA key. + /// + /// The signature padding scheme. + [Obsolete ("Use RsaSignaturePadding instead.")] + public RsaSignaturePaddingScheme RsaSignaturePaddingScheme { + get { return RsaSignaturePadding?.Scheme ?? RsaSignaturePaddingScheme.Pkcs1; } + set { + switch (value) { + case RsaSignaturePaddingScheme.Pkcs1: RsaSignaturePadding = RsaSignaturePadding.Pkcs1; break; + case RsaSignaturePaddingScheme.Pss: RsaSignaturePadding = RsaSignaturePadding.Pss; break; + default: throw new ArgumentOutOfRangeException (nameof (value)); + } + } + } + + /// + /// Get or set the RSA signature padding. + /// + /// + /// Gets or sets the signature padding to use for signing when + /// the is an RSA key. + /// + /// The signature padding scheme. + public RsaSignaturePadding RsaSignaturePadding { + get; set; + } + + /// + /// Gets the signer identifier type. + /// + /// + /// Specifies how the certificate should be looked up on the recipient's end. + /// + /// The signer identifier type. + public SubjectIdentifierType SignerIdentifierType { + get; private set; + } + + /// + /// Get or set the signed attributes. + /// + /// + /// A table of attributes that should be included in the signature. + /// + /// The signed attributes. + public AttributeTable SignedAttributes { + get; set; + } + + /// + /// Get or set the unsigned attributes. + /// + /// + /// A table of attributes that should not be signed in the signature, + /// but still included in transport. + /// + /// The unsigned attributes. + public AttributeTable UnsignedAttributes { + get; set; + } + } +} diff --git a/src/MimeKit/Cryptography/CryptographyContext.cs b/src/MimeKit/Cryptography/CryptographyContext.cs new file mode 100644 index 0000000..60bed14 --- /dev/null +++ b/src/MimeKit/Cryptography/CryptographyContext.cs @@ -0,0 +1,648 @@ +// +// CryptographyContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MimeKit.Cryptography { + /// + /// An abstract cryptography context. + /// + /// + /// Generally speaking, applications should not use a + /// directly, but rather via higher level APIs such as , + /// and . + /// + public abstract class CryptographyContext : IDisposable + { + const string SubclassAndRegisterFormat = "You need to subclass {0} and then register it with MimeKit.Cryptography.CryptographyContext.Register()."; + static Func SecureMimeContextFactory; + static Func PgpContextFactory; + static readonly object mutex = new object (); + + EncryptionAlgorithm[] encryptionAlgorithmRank; + DigestAlgorithm[] digestAlgorithmRank; + + int enabledEncryptionAlgorithms; + int enabledDigestAlgorithms; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// By default, only the 3DES encryption algorithm and the SHA-1 digest algorithm are enabled. + /// + protected CryptographyContext () + { + encryptionAlgorithmRank = new[] { + EncryptionAlgorithm.TripleDes + }; + + Enable (EncryptionAlgorithm.TripleDes); + + digestAlgorithmRank = new[] { + DigestAlgorithm.Sha1 + }; + + Enable (DigestAlgorithm.Sha1); + } + + /// + /// Get the signature protocol. + /// + /// + /// The signature protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The signature protocol. + public abstract string SignatureProtocol { get; } + + /// + /// Get the encryption protocol. + /// + /// + /// The encryption protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The encryption protocol. + public abstract string EncryptionProtocol { get; } + + /// + /// Get the key exchange protocol. + /// + /// + /// The key exchange protocol is really only used for OpenPGP. + /// + /// The key exchange protocol. + public abstract string KeyExchangeProtocol { get; } + +#if NOT_YET + /// + /// Gets or sets a value indicating whether this allows online + /// certificate retrieval. + /// + /// true if online certificate retrieval should be allowed; otherwise, false. + public bool AllowOnlineCertificateRetrieval { get; set; } + + /// + /// Gets or sets the online certificate retrieval timeout. + /// + /// The online certificate retrieval timeout. + public TimeSpan OnlineCertificateRetrievalTimeout { get; set; } +#endif + + /// + /// Get the preferred rank order for the encryption algorithms; from the most preferred to the least. + /// + /// + /// Gets the preferred rank order for the encryption algorithms; from the most preferred to the least. + /// + /// The preferred encryption algorithm ranking. + protected EncryptionAlgorithm[] EncryptionAlgorithmRank { + get { return encryptionAlgorithmRank; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0) + throw new ArgumentException ("The array of encryption algorithms cannot be empty.", nameof (value)); + + encryptionAlgorithmRank = value; + } + } + + /// + /// Get the enabled encryption algorithms in ranked order. + /// + /// + /// Gets the enabled encryption algorithms in ranked order. + /// + /// The enabled encryption algorithms. + public EncryptionAlgorithm[] EnabledEncryptionAlgorithms { + get { + var algorithms = new List (); + + foreach (var algorithm in EncryptionAlgorithmRank) { + if (IsEnabled (algorithm)) + algorithms.Add (algorithm); + } + + return algorithms.ToArray (); + } + } + + /// + /// Enable the encryption algorithm. + /// + /// + /// Enables the encryption algorithm. + /// + /// The encryption algorithm. + public void Enable (EncryptionAlgorithm algorithm) + { + enabledEncryptionAlgorithms |= 1 << (int) algorithm; + } + + /// + /// Disable the encryption algorithm. + /// + /// + /// Disables the encryption algorithm. + /// + /// The encryption algorithm. + public void Disable (EncryptionAlgorithm algorithm) + { + enabledEncryptionAlgorithms &= ~(1 << (int) algorithm); + } + + /// + /// Check whether the specified encryption algorithm is enabled. + /// + /// + /// Determines whether the specified encryption algorithm is enabled. + /// + /// true if the specified encryption algorithm is enabled; otherwise, false. + /// The encryption algorithm. + public bool IsEnabled (EncryptionAlgorithm algorithm) + { + return (enabledEncryptionAlgorithms & (1 << (int) algorithm)) != 0; + } + + /// + /// Get the preferred rank order for the digest algorithms; from the most preferred to the least. + /// + /// + /// Gets the preferred rank order for the digest algorithms; from the most preferred to the least. + /// + /// The preferred encryption algorithm ranking. + protected DigestAlgorithm[] DigestAlgorithmRank { + get { return digestAlgorithmRank; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0) + throw new ArgumentException ("The array of digest algorithms cannot be empty.", nameof (value)); + + digestAlgorithmRank = value; + } + } + + /// + /// Get the enabled digest algorithms in ranked order. + /// + /// + /// Gets the enabled digest algorithms in ranked order. + /// + /// The enabled encryption algorithms. + public DigestAlgorithm[] EnabledDigestAlgorithms { + get { + var algorithms = new List (); + + foreach (var algorithm in DigestAlgorithmRank) { + if (IsEnabled (algorithm)) + algorithms.Add (algorithm); + } + + return algorithms.ToArray (); + } + } + + /// + /// Enable the digest algorithm. + /// + /// + /// Enables the digest algorithm. + /// + /// The digest algorithm. + public void Enable (DigestAlgorithm algorithm) + { + enabledDigestAlgorithms |= 1 << (int) algorithm; + } + + /// + /// Disable the digest algorithm. + /// + /// + /// Disables the digest algorithm. + /// + /// The digest algorithm. + public void Disable (DigestAlgorithm algorithm) + { + enabledDigestAlgorithms &= ~(1 << (int) algorithm); + } + + /// + /// Check whether the specified digest algorithm is enabled. + /// + /// + /// Determines whether the specified digest algorithm is enabled. + /// + /// true if the specified digest algorithm is enabled; otherwise, false. + /// The digest algorithm. + public bool IsEnabled (DigestAlgorithm algorithm) + { + return (enabledDigestAlgorithms & (1 << (int) algorithm)) != 0; + } + + /// + /// Check whether or not the specified protocol is supported by the . + /// + /// + /// Used in order to make sure that the protocol parameter value specified in either a multipart/signed + /// or multipart/encrypted part is supported by the supplied cryptography context. + /// + /// true if the protocol is supported; otherwise false + /// The protocol. + /// + /// is null. + /// + public abstract bool Supports (string protocol); + + /// + /// Get the string name of the digest algorithm for use with the micalg parameter of a multipart/signed part. + /// + /// + /// Maps the to the appropriate string identifier + /// as used by the micalg parameter value of a multipart/signed Content-Type + /// header. + /// + /// The micalg value. + /// The digest algorithm. + /// + /// is out of range. + /// + public abstract string GetDigestAlgorithmName (DigestAlgorithm micalg); + + /// + /// Get the digest algorithm from the micalg parameter value in a multipart/signed part. + /// + /// + /// Maps the micalg parameter value string back to the appropriate . + /// + /// The digest algorithm. + /// The micalg parameter value. + /// + /// is null. + /// + public abstract DigestAlgorithm GetDigestAlgorithm (string micalg); + + /// + /// Check whether or not a particular mailbox address can be used for signing. + /// + /// + /// Checks whether or not as particular mailbocx address can be used for signing. + /// + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. + /// + /// is null. + /// + public abstract bool CanSign (MailboxAddress signer); + + /// + /// Check whether or not the cryptography context can encrypt to a particular recipient. + /// + /// + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. + /// + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. + /// + public abstract bool CanEncrypt (MailboxAddress mailbox); + + /// + /// Cryptographically sign the content. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + public abstract MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content); + + /// + /// Verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public abstract DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public abstract Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A certificate could not be found for one or more of the . + /// + public abstract MimePart Encrypt (IEnumerable recipients, Stream content); + + /// + /// Decrypt the specified encryptedData. + /// + /// + /// Decrypts the specified encryptedData. + /// + /// The decrypted . + /// The encrypted data. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public abstract MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Imports the public certificates or keys from the specified stream. + /// + /// + /// Imports the public certificates or keys from the specified stream. + /// + /// The raw certificate or key data. + /// + /// is null. + /// + /// + /// Importing keys is not supported by this cryptography context. + /// + public abstract void Import (Stream stream); + + /// + /// Exports the keys for the specified mailboxes. + /// + /// + /// Exports the keys for the specified mailboxes. + /// + /// A new instance containing the exported keys. + /// The mailboxes. + /// + /// is null. + /// + /// + /// was empty. + /// + /// + /// Exporting keys is not supported by this cryptography context. + /// + public abstract MimePart Export (IEnumerable mailboxes); + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected virtual void Dispose (bool disposing) + { + } + + /// + /// Releases all resources used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + /// + /// Creates a new for the specified protocol. + /// + /// + /// Creates a new for the specified protocol. + /// The default types can over overridden by calling + /// the method with the preferred type. + /// + /// The for the protocol. + /// The protocol. + /// + /// is null. + /// + /// + /// There are no supported s that support + /// the specified . + /// + public static CryptographyContext Create (string protocol) + { + if (protocol == null) + throw new ArgumentNullException (nameof (protocol)); + + protocol = protocol.ToLowerInvariant (); + + lock (mutex) { + switch (protocol) { + case "application/x-pkcs7-signature": + case "application/pkcs7-signature": + case "application/x-pkcs7-mime": + case "application/pkcs7-mime": + case "application/x-pkcs7-keys": + case "application/pkcs7-keys": + if (SecureMimeContextFactory != null) + return SecureMimeContextFactory (); + + return new DefaultSecureMimeContext (); + case "application/x-pgp-signature": + case "application/pgp-signature": + case "application/x-pgp-encrypted": + case "application/pgp-encrypted": + case "application/x-pgp-keys": + case "application/pgp-keys": + if (PgpContextFactory != null) + return PgpContextFactory (); + + throw new NotSupportedException (string.Format (SubclassAndRegisterFormat, "MimeKit.Cryptography.OpenPgpContext or MimeKit.Cryptography.GnuPGContext")); + default: + throw new NotSupportedException (); + } + } + } + + /// + /// Registers a default or . + /// + /// + /// Registers the specified type as the default or + /// . + /// + /// A custom subclass of or + /// . + /// + /// is null. + /// + /// + /// is not a subclass of + /// or . + /// -or- + /// does not have a parameterless constructor. + /// + public static void Register (Type type) + { + if (type == null) + throw new ArgumentNullException (nameof (type)); + +#if NETSTANDARD1_3 || NETSTANDARD1_6 + var info = type.GetTypeInfo (); +#else + var info = type; +#endif + var ctor = type.GetConstructor (new Type[0]); + + if (ctor == null) + throw new ArgumentException ("The specified type must have a parameterless constructor.", nameof (type)); + + if (info.IsSubclassOf (typeof (SecureMimeContext))) { + lock (mutex) { + SecureMimeContextFactory = () => (SecureMimeContext) ctor.Invoke (new object[0]); + } + } else if (info.IsSubclassOf (typeof (OpenPgpContextBase))) { + lock (mutex) { + PgpContextFactory = () => (OpenPgpContextBase) ctor.Invoke (new object[0]); + } + } else { + throw new ArgumentException ("The specified type must be a subclass of SecureMimeContext or OpenPgpContext.", nameof (type)); + } + } + + /// + /// Registers a default factory. + /// + /// + /// Registers a factory that will return a new instance of the default . + /// + /// A factory that creates a new instance of . + /// + /// is null. + /// + public static void Register (Func factory) + { + if (factory == null) + throw new ArgumentNullException (nameof (factory)); + + lock (mutex) { + SecureMimeContextFactory = factory; + } + } + + /// + /// Registers a default factory. + /// + /// + /// Registers a factory that will return a new instance of the default . + /// + /// A factory that creates a new instance of . + /// + /// is null. + /// + public static void Register (Func factory) + { + if (factory == null) + throw new ArgumentNullException(nameof (factory)); + + lock (mutex) { + PgpContextFactory = factory; + } + } + } +} diff --git a/src/MimeKit/Cryptography/DbExtensions.cs b/src/MimeKit/Cryptography/DbExtensions.cs new file mode 100644 index 0000000..0d66fa1 --- /dev/null +++ b/src/MimeKit/Cryptography/DbExtensions.cs @@ -0,0 +1,50 @@ +// +// DbExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Data.Common; + +namespace MimeKit.Cryptography { + /// + /// Useful extensions for working with System.Data types. + /// + static class DbExtensions + { + /// + /// Creates a with the specified name and value and then adds it to the command's parameters. + /// + /// The database command. + /// The parameter name. + /// The parameter value. + public static int AddParameterWithValue (this DbCommand command, string name, object value) + { + var parameter = command.CreateParameter (); + parameter.ParameterName = name; + parameter.Value = value; + + return command.Parameters.Add (parameter); + } + } +} diff --git a/src/MimeKit/Cryptography/DefaultSecureMimeContext.cs b/src/MimeKit/Cryptography/DefaultSecureMimeContext.cs new file mode 100644 index 0000000..ceee70c --- /dev/null +++ b/src/MimeKit/Cryptography/DefaultSecureMimeContext.cs @@ -0,0 +1,668 @@ +// +// DefaultSecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// A default implementation that uses + /// an SQLite database as a certificate and private key store. + /// + /// + /// The default S/MIME context is designed to be usable on any platform + /// where there exists a .NET runtime by storing certificates, CRLs, and + /// (encrypted) private keys in a SQL database. + /// + public class DefaultSecureMimeContext : BouncyCastleSecureMimeContext + { + const X509CertificateRecordFields CmsRecipientFields = X509CertificateRecordFields.Algorithms | X509CertificateRecordFields.Certificate; + const X509CertificateRecordFields CmsSignerFields = X509CertificateRecordFields.Certificate | X509CertificateRecordFields.PrivateKey; + const X509CertificateRecordFields AlgorithmFields = X509CertificateRecordFields.Id | X509CertificateRecordFields.Algorithms | X509CertificateRecordFields.AlgorithmsUpdated; + const X509CertificateRecordFields ImportPkcs12Fields = AlgorithmFields | X509CertificateRecordFields.Trusted | X509CertificateRecordFields.PrivateKey; + + /// + /// The default database path for certificates, private keys and CRLs. + /// + /// + /// On Microsoft Windows-based systems, this path will be something like C:\Users\UserName\AppData\Roaming\mimekit\smime.db. + /// On Unix systems such as Linux and Mac OS X, this path will be ~/.mimekit/smime.db. + /// + public static readonly string DefaultDatabasePath; + + readonly IX509CertificateDatabase dbase; + + static DefaultSecureMimeContext () + { + string path; + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (Path.DirectorySeparatorChar == '\\') { + var appData = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + path = Path.Combine (appData, "Roaming\\mimekit"); + } else { + var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal); + path = Path.Combine (home, ".mimekit"); + } +#else + path = ".mimekit"; +#endif + + DefaultDatabasePath = Path.Combine (path, "smime.db"); + } + + static void CheckIsAvailable () + { + if (!SqliteCertificateDatabase.IsAvailable) { + const string format = "SQLite is not available. Install the {0} nuget."; +#if NETSTANDARD1_3 || NETSTANDARD1_6 + throw new NotSupportedException (string.Format (format, "Microsoft.Data.Sqlite")); +#else + throw new NotSupportedException (string.Format (format, "System.Data.SQLite")); +#endif + } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Allows the program to specify its own location for the SQLite database. If the file does not exist, + /// it will be created and the necessary tables and indexes will be constructed. + /// Requires linking with Mono.Data.Sqlite. + /// + /// The path to the SQLite database. + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// + /// + /// Mono.Data.Sqlite is not available. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An error occurred reading the file. + /// + public DefaultSecureMimeContext (string fileName, string password) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + CheckIsAvailable (); + + var dir = Path.GetDirectoryName (fileName); + var exists = File.Exists (fileName); + + if (!string.IsNullOrEmpty (dir) && !Directory.Exists (dir)) + Directory.CreateDirectory (dir); + + dbase = new SqliteCertificateDatabase (fileName, password); + + if (!exists) { + // TODO: initialize our dbase with some root CA certificates. + } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Allows the program to specify its own password for the default database. + /// Requires linking with Mono.Data.Sqlite. + /// + /// The password used for encrypting and decrypting the private keys. + /// + /// Mono.Data.Sqlite is not available. + /// + /// + /// The user does not have access to read the database at the default location. + /// + /// + /// An error occurred reading the database at the default location. + /// + public DefaultSecureMimeContext (string password) : this (DefaultDatabasePath, password) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Not recommended for production use as the password to unlock the private keys is hard-coded. + /// Requires linking with Mono.Data.Sqlite. + /// + /// + /// Mono.Data.Sqlite is not available. + /// + /// + /// The user does not have access to read the database at the default location. + /// + /// + /// An error occurred reading the database at the default location. + /// + public DefaultSecureMimeContext () : this (DefaultDatabasePath, "no.secret") + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is useful for supplying a custom . + /// + /// The certificate database. + /// + /// is null. + /// + public DefaultSecureMimeContext (IX509CertificateDatabase database) + { + if (database == null) + throw new ArgumentNullException (nameof (database)); + + dbase = database; + } + + /// + /// Check whether or not a particular mailbox address can be used for signing. + /// + /// + /// Checks whether or not as particular mailbocx address can be used for signing. + /// + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. + /// + /// is null. + /// + public override bool CanSign (MailboxAddress signer) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + foreach (var record in dbase.Find (signer, DateTime.UtcNow, true, CmsSignerFields)) { + if (record.KeyUsage != X509KeyUsageFlags.None && (record.KeyUsage & SecureMimeContext.DigitalSignatureKeyUsageFlags) == 0) + continue; + + return true; + } + + return false; + } + + /// + /// Check whether or not the cryptography context can encrypt to a particular recipient. + /// + /// + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. + /// + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. + /// + public override bool CanEncrypt (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, false, CmsRecipientFields)) { + if (record.KeyUsage != 0 && (record.KeyUsage & X509KeyUsageFlags.KeyEncipherment) == 0) + continue; + + return true; + } + + return false; + } + +#region implemented abstract members of SecureMimeContext + + /// + /// Gets the X.509 certificate matching the specified selector. + /// + /// + /// Gets the first certificate that matches the specified selector. + /// + /// The certificate on success; otherwise null. + /// The search criteria for the certificate. + protected override X509Certificate GetCertificate (IX509Selector selector) + { + return dbase.FindCertificates (selector).FirstOrDefault (); + } + + /// + /// Gets the private key for the certificate matching the specified selector. + /// + /// + /// Gets the private key for the first certificate that matches the specified selector. + /// + /// The private key on success; otherwise null. + /// The search criteria for the private key. + protected override AsymmetricKeyParameter GetPrivateKey (IX509Selector selector) + { + return dbase.FindPrivateKeys (selector).FirstOrDefault (); + } + + /// + /// Gets the trusted anchors. + /// + /// + /// A trusted anchor is a trusted root-level X.509 certificate, + /// generally issued by a Certificate Authority (CA). + /// + /// The trusted anchors. + protected override Org.BouncyCastle.Utilities.Collections.HashSet GetTrustedAnchors () + { + var anchors = new Org.BouncyCastle.Utilities.Collections.HashSet (); + var selector = new X509CertStoreSelector (); + var keyUsage = new bool[9]; + + keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true; + selector.KeyUsage = keyUsage; + + foreach (var record in dbase.Find (selector, true, X509CertificateRecordFields.Certificate)) + anchors.Add (new TrustAnchor (record.Certificate, null)); + + return anchors; + } + + /// + /// Gets the intermediate certificates. + /// + /// + /// An intermediate certificate is any certificate that exists between the root + /// certificate issued by a Certificate Authority (CA) and the certificate at + /// the end of the chain. + /// + /// The intermediate certificates. + protected override IX509Store GetIntermediateCertificates () + { + //var intermediates = new X509CertificateStore (); + //var selector = new X509CertStoreSelector (); + //var keyUsage = new bool[9]; + + //keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true; + //selector.KeyUsage = keyUsage; + + //foreach (var record in dbase.Find (selector, false, X509CertificateRecordFields.Certificate)) { + // if (!record.Certificate.IsSelfSigned ()) + // intermediates.Add (record.Certificate); + //} + + //return intermediates; + return dbase; + } + + /// + /// Gets the certificate revocation lists. + /// + /// + /// A Certificate Revocation List (CRL) is a list of certificate serial numbers issued + /// by a particular Certificate Authority (CA) that have been revoked, either by the CA + /// itself or by the owner of the revoked certificate. + /// + /// The certificate revocation lists. + protected override IX509Store GetCertificateRevocationLists () + { + return dbase.GetCrlStore (); + } + + /// + /// Get the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// + /// Gets the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// The date & time for the next update (in UTC). + /// The issuer. + protected override DateTime GetNextCertificateRevocationListUpdate (X509Name issuer) + { + var nextUpdate = DateTime.MinValue.ToUniversalTime (); + + foreach (var record in dbase.Find (issuer, X509CrlRecordFields.NextUpdate)) + nextUpdate = record.NextUpdate > nextUpdate ? record.NextUpdate : nextUpdate; + + return nextUpdate; + } + + /// + /// Gets the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate certificate and + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address for database lookups. + /// + /// A . + /// The recipient's mailbox address. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox) + { + foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, false, CmsRecipientFields)) { + if (record.KeyUsage != 0 && (record.KeyUsage & X509KeyUsageFlags.KeyEncipherment) == 0) + continue; + + var recipient = new CmsRecipient (record.Certificate); + + if (record.Algorithms != null) + recipient.EncryptionAlgorithms = record.Algorithms; + + return recipient; + } + + throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found."); + } + + /// + /// Gets the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate signing certificate + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address for database lookups. + /// + /// A . + /// The signer's mailbox address. + /// The preferred digest algorithm. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo) + { + AsymmetricKeyParameter privateKey = null; + X509Certificate certificate = null; + + foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, true, CmsSignerFields)) { + if (record.KeyUsage != X509KeyUsageFlags.None && (record.KeyUsage & DigitalSignatureKeyUsageFlags) == 0) + continue; + + certificate = record.Certificate; + privateKey = record.PrivateKey; + break; + } + + if (certificate != null && privateKey != null) { + var signer = new CmsSigner (BuildCertificateChain (certificate), privateKey); + signer.DigestAlgorithm = digestAlgo; + + return signer; + } + + throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found."); + } + + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// The certificate. + /// The encryption algorithm capabilities of the client (in preferred order). + /// The timestamp in coordinated universal time (UTC). + protected override void UpdateSecureMimeCapabilities (X509Certificate certificate, EncryptionAlgorithm[] algorithms, DateTime timestamp) + { + X509CertificateRecord record; + + if ((record = dbase.Find (certificate, AlgorithmFields)) == null) { + record = new X509CertificateRecord (certificate); + record.AlgorithmsUpdated = timestamp; + record.Algorithms = algorithms; + + dbase.Add (record); + } else if (timestamp > record.AlgorithmsUpdated) { + record.AlgorithmsUpdated = timestamp; + record.Algorithms = algorithms; + + dbase.Update (record, AlgorithmFields); + } + } + + /// + /// Imports a certificate. + /// + /// + /// Imports the specified certificate into the database. + /// + /// The certificate. + /// + /// is null. + /// + public override void Import (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (dbase.Find (certificate, X509CertificateRecordFields.Id) == null) + dbase.Add (new X509CertificateRecord (certificate)); + } + + /// + /// Imports a certificate revocation list. + /// + /// + /// Imports the specified certificate revocation list. + /// + /// The certificate revocation list. + /// + /// is null. + /// + public override void Import (X509Crl crl) + { + if (crl == null) + throw new ArgumentNullException (nameof (crl)); + + // check for an exact match... + if (dbase.Find (crl, X509CrlRecordFields.Id) != null) + return; + + const X509CrlRecordFields fields = ~X509CrlRecordFields.Crl; + var obsolete = new List (); + var delta = crl.IsDelta (); + + // scan over our list of CRLs by the same issuer to check if this CRL obsoletes any + // older CRLs or if there are any newer CRLs that obsolete that obsolete this one. + foreach (var record in dbase.Find (crl.IssuerDN, fields)) { + if (!record.IsDelta && record.ThisUpdate >= crl.ThisUpdate) { + // we have a complete CRL that obsoletes this CRL + return; + } + + if (!delta) + obsolete.Add (record); + } + + // remove any obsoleted CRLs + foreach (var record in obsolete) + dbase.Remove (record); + + dbase.Add (new X509CrlRecord (crl)); + } + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// + /// Imports all of the certificates and keys from the pkcs12-encoded stream. + /// + /// The raw certificate and key data. + /// The password to unlock the data. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override void Import (Stream stream, string password) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ()); + var enabledAlgorithms = EnabledEncryptionAlgorithms; + X509CertificateRecord record; + + foreach (string alias in pkcs12.Aliases) { + if (pkcs12.IsKeyEntry (alias)) { + var chain = pkcs12.GetCertificateChain (alias); + var entry = pkcs12.GetKey (alias); + int startIndex = 0; + + if (entry.Key.IsPrivate) { + if ((record = dbase.Find (chain[0].Certificate, ImportPkcs12Fields)) == null) { + record = new X509CertificateRecord (chain[0].Certificate, entry.Key); + record.AlgorithmsUpdated = DateTime.UtcNow; + record.Algorithms = enabledAlgorithms; + record.IsTrusted = true; + dbase.Add (record); + } else { + record.AlgorithmsUpdated = DateTime.UtcNow; + record.Algorithms = enabledAlgorithms; + if (record.PrivateKey == null) + record.PrivateKey = entry.Key; + record.IsTrusted = true; + dbase.Update (record, ImportPkcs12Fields); + } + + startIndex = 1; + } + + for (int i = startIndex; i < chain.Length; i++) + Import (chain[i].Certificate, true); + } else if (pkcs12.IsCertificateEntry (alias)) { + var entry = pkcs12.GetCertificate (alias); + + Import (entry.Certificate, true); + } + } + } + + #endregion + + /// + /// Imports a certificate. + /// + /// + /// Imports the certificate. + /// If the certificate already exists in the database and is true, + /// then the IsTrusted state is updated otherwise the certificate is added to the database with the + /// specified trust. + /// + /// The certificate. + /// true if the certificate is trusted; otherwise, false. + /// + /// is null. + /// + public void Import (X509Certificate certificate, bool trusted) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + X509CertificateRecord record; + + if ((record = dbase.Find (certificate, X509CertificateRecordFields.Id | X509CertificateRecordFields.Trusted)) != null) { + if (trusted && !record.IsTrusted) { + record.IsTrusted = trusted; + dbase.Update (record, X509CertificateRecordFields.Trusted); + } + + return; + } + + record = new X509CertificateRecord (certificate); + record.IsTrusted = trusted; + dbase.Add (record); + } + + /// + /// Imports a DER-encoded certificate stream. + /// + /// + /// Imports the certificate(s). + /// + /// The raw certificate(s). + /// true if the certificates are trusted; othewrwise, false. + /// + /// is null. + /// + public void Import (Stream stream, bool trusted) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new X509CertificateParser (); + + foreach (X509Certificate certificate in parser.ReadCertificates (stream)) + Import (certificate, trusted); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + dbase.Dispose (); + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/DigestAlgorithm.cs b/src/MimeKit/Cryptography/DigestAlgorithm.cs new file mode 100644 index 0000000..154f73a --- /dev/null +++ b/src/MimeKit/Cryptography/DigestAlgorithm.cs @@ -0,0 +1,112 @@ +// +// DigestAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// A digest algorithm. + /// + /// + /// Digest algorithms are secure hashing algorithms that are used + /// to generate unique fixed-length signatures for arbitrary data. + /// The most commonly used digest algorithms are currently MD5 + /// and SHA-1, however, MD5 was successfully broken in 2008 and should + /// be avoided. In late 2013, Microsoft announced that they would be + /// retiring their use of SHA-1 in their products by 2016 with the + /// assumption that its days as an unbroken digest algorithm were + /// numbered. It is speculated that the SHA-1 digest algorithm will + /// be vulnerable to collisions, and thus no longer considered secure, + /// by 2018. + /// Microsoft and other vendors plan to move to the SHA-2 suite of + /// digest algorithms which includes the following 4 variants: SHA-224, + /// SHA-256, SHA-384, and SHA-512. + /// + public enum DigestAlgorithm { + /// + /// No digest algorithm specified. + /// + None = 0, + + /// + /// The MD5 digest algorithm. + /// + MD5 = 1, + + /// + /// The SHA-1 digest algorithm. + /// + Sha1 = 2, + + /// + /// The Ripe-MD/160 digest algorithm. + /// + RipeMD160 = 3, + + /// + /// The double-SHA digest algorithm. + /// + DoubleSha = 4, + + /// + /// The MD2 digest algorithm. + /// + MD2 = 5, + + /// + /// The TIGER/192 digest algorithm. + /// + Tiger192 = 6, + + /// + /// The HAVAL 5-pass 160-bit digest algorithm. + /// + Haval5160 = 7, + + /// + /// The SHA-256 digest algorithm. + /// + Sha256 = 8, + + /// + /// The SHA-384 digest algorithm. + /// + Sha384 = 9, + + /// + /// The SHA-512 digest algorithm. + /// + Sha512 = 10, + + /// + /// The SHA-224 digest algorithm. + /// + Sha224 = 11, + + /// + /// The MD4 digest algorithm. + /// + MD4 = 301 + } +} diff --git a/src/MimeKit/Cryptography/DigitalSignatureCollection.cs b/src/MimeKit/Cryptography/DigitalSignatureCollection.cs new file mode 100644 index 0000000..211dfe4 --- /dev/null +++ b/src/MimeKit/Cryptography/DigitalSignatureCollection.cs @@ -0,0 +1,54 @@ +// +// DigitalSignatureCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MimeKit.Cryptography { + /// + /// A collection of digital signatures. + /// + /// + /// When verifying a digitally signed MIME part such as a + /// or a , you will get back a collection of + /// digital signatures. Typically, a signed message will only have a single signature + /// (created by the sender of the message), but it is possible for there to be + /// multiple signatures. + /// + public class DigitalSignatureCollection : ReadOnlyCollection + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The signatures. + public DigitalSignatureCollection (IList signatures) : base (signatures) + { + } + } +} diff --git a/src/MimeKit/Cryptography/DigitalSignatureVerifyException.cs b/src/MimeKit/Cryptography/DigitalSignatureVerifyException.cs new file mode 100644 index 0000000..cd491eb --- /dev/null +++ b/src/MimeKit/Cryptography/DigitalSignatureVerifyException.cs @@ -0,0 +1,147 @@ +// +// DigitalSignatureVerifyException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MimeKit.Cryptography { + /// + /// An exception that is thrown when an error occurrs in . + /// + /// + /// For more information about the error condition, check the property. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class DigitalSignatureVerifyException : Exception + { +#if SERIALIZABLE + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The stream context. + /// + /// is null. + /// + protected DigitalSignatureVerifyException (SerializationInfo info, StreamingContext context) : base (info, context) + { + KeyId = (long?) info.GetValue ("KeyId", typeof (long?)); + } +#endif + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The key identifier. + /// The error message. + /// The inner exception. + public DigitalSignatureVerifyException (long keyId, string message, Exception innerException) : base (message, innerException) + { + KeyId = keyId; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The key identifier. + /// The error message. + public DigitalSignatureVerifyException (long keyId, string message) : base (message) + { + KeyId = keyId; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The inner exception. + public DigitalSignatureVerifyException (string message, Exception innerException) : base (message, innerException) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + public DigitalSignatureVerifyException (string message) : base (message) + { + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("KeyId", KeyId, typeof (long?)); + } +#endif + + /// + /// Get the key identifier, if available. + /// + /// + /// Gets the key identifier, if available. + /// + /// The key identifier. + public long? KeyId { + get; private set; + } + } +} diff --git a/src/MimeKit/Cryptography/DkimBodyFilter.cs b/src/MimeKit/Cryptography/DkimBodyFilter.cs new file mode 100644 index 0000000..b675cc3 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimBodyFilter.cs @@ -0,0 +1,72 @@ +// +// DkimBodyFilterBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.IO.Filters; + +namespace MimeKit.Cryptography { + /// + /// A base implementation for DKIM body filters. + /// + /// + /// A base implementation for DKIM body filters. + /// + abstract class DkimBodyFilter : MimeFilterBase + { + /// + /// Get or set whether the last filtered character was a newline. + /// + /// + /// Gets or sets whether the last filtered character was a newline. + /// + internal protected bool LastWasNewLine; + + /// + /// Get or set whether the current line is empty. + /// + /// + /// Gets or sets whether the current line is empty. + /// + protected bool IsEmptyLine; + + /// + /// Get or set the number of consecutive empty lines encountered. + /// + /// + /// Gets or sets the number of consecutive empty lines encountered. + /// + protected int EmptyLines; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + protected DkimBodyFilter () + { + } + } +} diff --git a/src/MimeKit/Cryptography/DkimCanonicalizationAlgorithm.cs b/src/MimeKit/Cryptography/DkimCanonicalizationAlgorithm.cs new file mode 100644 index 0000000..67f0f75 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimCanonicalizationAlgorithm.cs @@ -0,0 +1,59 @@ +// +// DkimCanonicalizationAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// A DKIM canonicalization algorithm. + /// + /// + /// Empirical evidence demonstrates that some mail servers and relay systems + /// modify email in transit, potentially invalidating a signature. There are two + /// competing perspectives on such modifications. For most signers, mild modification + /// of email is immaterial to the authentication status of the email. For such signers, + /// a canonicalization algorithm that survives modest in-transit modification is + /// preferred. + /// Other signers demand that any modification of the email, however minor, + /// result in a signature verification failure. These signers prefer a canonicalization + /// algorithm that does not tolerate in-transit modification of the signed email. + /// + /// + /// + /// + public enum DkimCanonicalizationAlgorithm { + /// + /// The simple canonicalization algorithm tolerates almost no modification + /// by mail servers while the message is in-transit. + /// + Simple, + + /// + /// The relaxed canonicalization algorithm tolerates common modifications + /// by mail servers while the message is in-transit such as whitespace + /// replacement and header field line rewrapping. + /// + Relaxed + } +} diff --git a/src/MimeKit/Cryptography/DkimHashStream.cs b/src/MimeKit/Cryptography/DkimHashStream.cs new file mode 100644 index 0000000..b639b74 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimHashStream.cs @@ -0,0 +1,370 @@ +// +// DkimHashStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +#if ENABLE_NATIVE_DKIM +using System.Security.Cryptography; +#else +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +#endif + +namespace MimeKit.Cryptography { + /// + /// A DKIM hash stream. + /// + /// + /// A DKIM hash stream. + /// + class DkimHashStream : Stream + { +#if ENABLE_NATIVE_DKIM + HashAlgorithm digest; +#else + IDigest digest; +#endif + bool disposed; + int length; + int max; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The signature algorithm. + /// The max length of data to hash. + public DkimHashStream (DkimSignatureAlgorithm algorithm, int maxLength = -1) + { +#if ENABLE_NATIVE_DKIM + switch (algorithm) { + case DkimSignatureAlgorithm.Ed25519Sha256: + case DkimSignatureAlgorithm.RsaSha256: + digest = SHA256.Create (); + break; + default: + digest = SHA1.Create (); + break; + } +#else + switch (algorithm) { + case DkimSignatureAlgorithm.Ed25519Sha256: + case DkimSignatureAlgorithm.RsaSha256: + digest = new Sha256Digest (); + break; + default: + digest = new Sha1Digest (); + break; + } +#endif + + max = maxLength; + } + + /// + /// Generate the hash. + /// + /// + /// Generates the hash. + /// + /// The hash. + public byte[] GenerateHash () + { +#if ENABLE_NATIVE_DKIM + digest.TransformFinalBlock (new byte[0], 0, 0); + + return digest.Hash; +#else + var hash = new byte[digest.GetDigestSize ()]; + + digest.DoFinal (hash, 0); + + return hash; +#endif + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (DkimHashStream)); + } + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// A is not readable. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return false; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// A is always writable. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return true; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// A is not seekable. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Checks whether or not reading and writing to the stream can timeout. + /// + /// + /// Writing to a cannot timeout. + /// + /// true if reading and writing to the stream can timeout; otherwise, false. + public override bool CanTimeout { + get { return false; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// The length of a indicates the + /// number of bytes that have been written to it. + /// + /// The length of the stream in bytes. + /// + /// The stream has been disposed. + /// + public override long Length { + get { + CheckDisposed (); + + return length; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// Since it is possible to seek within a , + /// it is possible that the position will not always be identical to the + /// length of the stream, but typically it will be. + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return length; } + set { Seek (value, SeekOrigin.Begin); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reading from a is not supported. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support reading"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Increments the property by the number of bytes written. + /// If the updated position is greater than the current length of the stream, then + /// the property will be updated to be identical to the + /// position. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + int n = max >= 0 && length + count > max ? max - length : count; + +#if ENABLE_NATIVE_DKIM + digest.TransformBlock (buffer, offset, count, null, 0); +#else + digest.BlockUpdate (buffer, offset, n); +#endif + + length += n; + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Updates the within the stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + public override long Seek (long offset, SeekOrigin origin) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support seeking."); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Since a does not actually do anything other than + /// count bytes, this method is a no-op. + /// + /// + /// The stream has been disposed. + /// + public override void Flush () + { + CheckDisposed (); + + // nothing to do... + } + + /// + /// Sets the length of the stream. + /// + /// + /// Sets the to the specified value and updates + /// to the specified value if (and only if) + /// the current position is greater than the new length value. + /// + /// The desired length of the stream in bytes. + /// + /// is out of range. + /// + /// + /// The stream has been disposed. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support setting the length."); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { +#if ENABLE_NATIVE_DKIM + digest.Dispose (); +#endif + + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimPublicKeyLocatorBase.cs b/src/MimeKit/Cryptography/DkimPublicKeyLocatorBase.cs new file mode 100644 index 0000000..3edc6a9 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimPublicKeyLocatorBase.cs @@ -0,0 +1,189 @@ +// +// DkimPublicKeyLocatorBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MimeKit.Cryptography { + /// + /// A base class for implemnentations of . + /// + /// + /// The class provides a helpful + /// method for parsing DNS TXT records in order to extract the public key. + /// + /// + /// + /// + /// + /// + /// + public abstract class DkimPublicKeyLocatorBase : IDkimPublicKeyLocator + { + /// + /// Get the public key from a DNS TXT record. + /// + /// + /// Gets the public key from a DNS TXT record. + /// + /// The DNS TXT record. + /// The public key. + /// + /// The is null. + /// + /// + /// There was an error parsing the DNS TXT record. + /// + protected AsymmetricKeyParameter GetPublicKey (string txt) + { + AsymmetricKeyParameter pubkey; + string k = null, p = null; + int index = 0; + + if (txt == null) + throw new ArgumentNullException (nameof (txt)); + + // parse the response (will look something like: "k=rsa; p=") + while (index < txt.Length) { + while (index < txt.Length && char.IsWhiteSpace (txt[index])) + index++; + + if (index == txt.Length) + break; + + // find the end of the key + int startIndex = index; + while (index < txt.Length && txt[index] != '=') + index++; + + if (index == txt.Length) + break; + + var key = txt.Substring (startIndex, index - startIndex); + + // skip over the '=' + index++; + + // find the end of the value + startIndex = index; + while (index < txt.Length && txt[index] != ';') + index++; + + var value = txt.Substring (startIndex, index - startIndex); + + switch (key) { + case "k": + switch (value) { + case "rsa": case "ed25519": k = value; break; + default: throw new ParseException ($"Unknown public key algorithm: {value}", startIndex, index); + } + break; + case "p": + p = value.Replace (" ", ""); + break; + } + + // skip over the ';' + index++; + } + + if (k != null && p != null) { + if (k == "ed25519") { + var decoded = Convert.FromBase64String (p); + + return new Ed25519PublicKeyParameters (decoded, 0); + } + + var data = "-----BEGIN PUBLIC KEY-----\r\n" + p + "\r\n-----END PUBLIC KEY-----\r\n"; + var rawData = Encoding.ASCII.GetBytes (data); + + using (var stream = new MemoryStream (rawData, false)) { + using (var reader = new StreamReader (stream)) { + var pem = new PemReader (reader); + + pubkey = pem.ReadObject () as AsymmetricKeyParameter; + + if (pubkey != null) + return pubkey; + } + } + } + + throw new ParseException ("Public key parameters not found in DNS TXT record.", 0, txt.Length); + } + + /// + /// Locate and retrieve the public key for the given domain and selector. + /// + /// + /// Locates and retrieves the public key for the given domain and selector. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The public key. + /// A colon-separated list of query methods used to retrieve the public key. The default is "dns/txt". + /// The domain. + /// The selector. + /// The cancellation token. + public abstract AsymmetricKeyParameter LocatePublicKey (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously locate and retrieve the public key for the given domain and selector. + /// + /// + /// Locates and retrieves the public key for the given domain and selector. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The public key. + /// A colon-separated list of query methods used to retrieve the public key. The default is "dns/txt". + /// The domain. + /// The selector. + /// The cancellation token. + public abstract Task LocatePublicKeyAsync (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MimeKit/Cryptography/DkimRelaxedBodyFilter.cs b/src/MimeKit/Cryptography/DkimRelaxedBodyFilter.cs new file mode 100644 index 0000000..91905ac --- /dev/null +++ b/src/MimeKit/Cryptography/DkimRelaxedBodyFilter.cs @@ -0,0 +1,167 @@ +// +// DkimRelaxedBodyFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// A filter for the DKIM relaxed body canonicalization. + /// + /// + /// A filter for the DKIM relaxed body canonicalization. + /// + class DkimRelaxedBodyFilter : DkimBodyFilter + { + bool lwsp, cr; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public DkimRelaxedBodyFilter () + { + LastWasNewLine = true; + IsEmptyLine = true; + } + + unsafe int Filter (byte* inbuf, int length, byte* outbuf) + { + byte* inend = inbuf + length; + byte* outptr = outbuf; + byte* inptr = inbuf; + int count = 0; + + while (inptr < inend) { + if (*inptr == (byte) '\n') { + if (IsEmptyLine) { + EmptyLines++; + } else { + if (cr) { + *outptr++ = (byte) '\r'; + count++; + } + + *outptr++ = (byte) '\n'; + LastWasNewLine = true; + IsEmptyLine = true; + count++; + } + + lwsp = false; + cr = false; + } else { + if (cr) { + *outptr++ = (byte) '\r'; + cr = false; + count++; + } + + if (*inptr == (byte) '\r') { + lwsp = false; + cr = true; + } else if ((*inptr).IsBlank ()) { + lwsp = true; + } else { + if (EmptyLines > 0) { + // unwind our collection of empty lines + while (EmptyLines > 0) { + *outptr++ = (byte) '\r'; + *outptr++ = (byte) '\n'; + EmptyLines--; + count += 2; + } + } + + if (lwsp) { + // collapse lwsp to a single space + *outptr++ = (byte) ' '; + lwsp = false; + count++; + } + + LastWasNewLine = false; + IsEmptyLine = false; + + *outptr++ = *inptr; + count++; + } + } + + inptr++; + } + + return count; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + EnsureOutputSize (length + (lwsp ? 1 : 0) + (EmptyLines * 2) + (cr ? 1 : 0) + 1, false); + + unsafe { + fixed (byte* inptr = input, outptr = OutputBuffer) { + outputLength = Filter (inptr + startIndex, length, outptr); + } + } + + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + LastWasNewLine = true; + IsEmptyLine = true; + EmptyLines = 0; + lwsp = false; + cr = false; + + base.Reset (); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimSignatureAlgorithm.cs b/src/MimeKit/Cryptography/DkimSignatureAlgorithm.cs new file mode 100644 index 0000000..5b38ddc --- /dev/null +++ b/src/MimeKit/Cryptography/DkimSignatureAlgorithm.cs @@ -0,0 +1,53 @@ +// +// DkimSignatureAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// A DKIM signature algorithm. + /// + /// + /// A DKIM signature algorithm. + /// + /// + /// + /// + public enum DkimSignatureAlgorithm { + /// + /// The RSA-SHA1 signature algorithm. + /// + RsaSha1, + + /// + /// The RSA-SHA256 signature algorithm. + /// + RsaSha256, + + /// + /// The Ed25519-SHA256 signature algorithm. + /// + Ed25519Sha256 + } +} diff --git a/src/MimeKit/Cryptography/DkimSignatureStream.cs b/src/MimeKit/Cryptography/DkimSignatureStream.cs new file mode 100644 index 0000000..83badb5 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimSignatureStream.cs @@ -0,0 +1,361 @@ +// +// DkimSignatureStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; + +using Org.BouncyCastle.Crypto; + +namespace MimeKit.Cryptography { + /// + /// A DKIM signature stream. + /// + /// + /// A DKIM signature stream. + /// + class DkimSignatureStream : Stream + { + bool disposed; + long length; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The digest signer. + /// + /// is null. + /// + public DkimSignatureStream (ISigner signer) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + Signer = signer; + } + + /// + /// Get the digest signer. + /// + /// + /// Gets the digest signer. + /// + /// The signer. + public ISigner Signer { + get; private set; + } + + /// + /// Generate the signature. + /// + /// + /// Generates the signature. + /// + /// The signature. + public byte[] GenerateSignature () + { + return Signer.GenerateSignature (); + } + + /// + /// Verify the DKIM signature. + /// + /// + /// Verifies the DKIM signature. + /// + /// true if signature is valid; otherwise, false. + /// The base64 encoded DKIM signature from the b= parameter. + /// + /// is null. + /// + public bool VerifySignature (string signature) + { + if (signature == null) + throw new ArgumentNullException (nameof (signature)); + + var rawSignature = Convert.FromBase64String (signature); + + return Signer.VerifySignature (rawSignature); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (DkimSignatureStream)); + } + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// A is not readable. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return false; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// A is always writable. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return true; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// A is not seekable. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Checks whether or not reading and writing to the stream can timeout. + /// + /// + /// Writing to a cannot timeout. + /// + /// true if reading and writing to the stream can timeout; otherwise, false. + public override bool CanTimeout { + get { return false; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// The length of a indicates the + /// number of bytes that have been written to it. + /// + /// The length of the stream in bytes. + /// + /// The stream has been disposed. + /// + public override long Length { + get { + CheckDisposed (); + + return length; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// Since it is possible to seek within a , + /// it is possible that the position will not always be identical to the + /// length of the stream, but typically it will be. + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return length; } + set { Seek (value, SeekOrigin.Begin); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reading from a is not supported. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support reading"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Increments the property by the number of bytes written. + /// If the updated position is greater than the current length of the stream, then + /// the property will be updated to be identical to the + /// position. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + Signer.BlockUpdate (buffer, offset, count); + + length += count; + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Updates the within the stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + public override long Seek (long offset, SeekOrigin origin) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support seeking."); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Since a does not actually do anything other than + /// count bytes, this method is a no-op. + /// + /// + /// The stream has been disposed. + /// + public override void Flush () + { + CheckDisposed (); + + // nothing to do... + } + + /// + /// Sets the length of the stream. + /// + /// + /// Sets the to the specified value and updates + /// to the specified value if (and only if) + /// the current position is greater than the new length value. + /// + /// The desired length of the stream in bytes. + /// + /// is out of range. + /// + /// + /// The stream has been disposed. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support setting the length."); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { +#if ENABLE_NATIVE_DKIM + var sss = Signer as SystemSecuritySigner; + + if (sss != null) + sss.Dispose (); +#endif + + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimSigner.cs b/src/MimeKit/Cryptography/DkimSigner.cs new file mode 100644 index 0000000..8b86833 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimSigner.cs @@ -0,0 +1,483 @@ +// +// DkimSigner.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// A DKIM signer. + /// + /// + /// A DKIM signer. + /// + /// + /// + /// + public class DkimSigner : DkimSignerBase + { + static readonly string[] DkimShouldNotInclude = { "return-path", "received", "comments", "keywords", "bcc", "resent-bcc", "dkim-signature" }; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// + protected DkimSigner (string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : base (domain, selector, algorithm) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// The signer's private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a private key. + /// + public DkimSigner (AsymmetricKeyParameter key, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (!key.IsPrivate) + throw new ArgumentException ("The key must be a private key.", nameof (key)); + + PrivateKey = key; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// + /// + /// + /// The file containing the private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The file did not contain a private key. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + public DkimSigner (string fileName, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The file name cannot be empty.", nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + PrivateKey = LoadPrivateKey (stream); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// The stream containing the private key. + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The file did not contain a private key. + /// + /// + /// An I/O error occurred. + /// + public DkimSigner (Stream stream, string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) : this (domain, selector, algorithm) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + PrivateKey = LoadPrivateKey (stream); + } + + /// + /// Get or set the agent or user identifier. + /// + /// + /// Gets or sets the agent or user identifier. + /// + /// + /// + /// + /// The agent or user identifier. + public string AgentOrUserIdentifier { + get; set; + } + + /// + /// Get or set the public key query method. + /// + /// + /// Gets or sets the public key query method. + /// The value should be a colon-separated list of query methods used to + /// retrieve the public key (plain-text; OPTIONAL, default is "dns/txt"). Each + /// query method is of the form "type[/options]", where the syntax and + /// semantics of the options depend on the type and specified options. + /// + /// + /// + /// + /// The public key query method. + public string QueryMethod { + get; set; + } + + /// + /// Get the timestamp value. + /// + /// + /// Gets the timestamp to use as the t= value in the DKIM-Signature header. + /// + /// A value representing the timestamp value. + protected virtual long GetTimestamp () + { + return (long) (DateTime.UtcNow - DateUtils.UnixEpoch).TotalSeconds; + } + + void DkimSign (FormatOptions options, MimeMessage message, IList headers) + { + var value = new StringBuilder ("v=1"); + var t = GetTimestamp (); + byte[] signature, hash; + Header dkim; + + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + options.EnsureNewLine = true; + + switch (SignatureAlgorithm) { + case DkimSignatureAlgorithm.Ed25519Sha256: + value.Append ("; a=ed25519-sha256"); + break; + case DkimSignatureAlgorithm.RsaSha256: + value.Append ("; a=rsa-sha256"); + break; + default: + value.Append ("; a=rsa-sha1"); + break; + } + + value.AppendFormat ("; d={0}; s={1}", Domain, Selector); + value.AppendFormat ("; c={0}/{1}", + HeaderCanonicalizationAlgorithm.ToString ().ToLowerInvariant (), + BodyCanonicalizationAlgorithm.ToString ().ToLowerInvariant ()); + if (!string.IsNullOrEmpty (QueryMethod)) + value.AppendFormat ("; q={0}", QueryMethod); + if (!string.IsNullOrEmpty (AgentOrUserIdentifier)) + value.AppendFormat ("; i={0}", AgentOrUserIdentifier); + value.AppendFormat ("; t={0}", t); + + using (var stream = new DkimSignatureStream (CreateSigningContext ())) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + // write the specified message headers + DkimVerifierBase.WriteHeaders (options, message, headers, HeaderCanonicalizationAlgorithm, filtered); + + value.AppendFormat ("; h={0}", string.Join (":", headers.ToArray ())); + + hash = message.HashBody (options, SignatureAlgorithm, BodyCanonicalizationAlgorithm, -1); + value.AppendFormat ("; bh={0}", Convert.ToBase64String (hash)); + value.Append ("; b="); + + dkim = new Header (HeaderId.DkimSignature, value.ToString ()); + message.Headers.Insert (0, dkim); + + switch (HeaderCanonicalizationAlgorithm) { + case DkimCanonicalizationAlgorithm.Relaxed: + DkimVerifierBase.WriteHeaderRelaxed (options, filtered, dkim, true); + break; + default: + DkimVerifierBase.WriteHeaderSimple (options, filtered, dkim, true); + break; + } + + filtered.Flush (); + } + + signature = stream.GenerateSignature (); + + dkim.Value += Convert.ToBase64String (signature); + } + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + public void Sign (FormatOptions options, MimeMessage message, IList headers) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + var fields = new string[headers.Count]; + var containsFrom = false; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i] == null) + throw new ArgumentException ("The list of headers cannot contain null.", nameof (headers)); + + if (headers[i].Length == 0) + throw new ArgumentException ("The list of headers cannot contain empty string.", nameof (headers)); + + fields[i] = headers[i].ToLowerInvariant (); + + if (DkimShouldNotInclude.Contains (fields[i])) + throw new ArgumentException (string.Format ("The list of headers to sign SHOULD NOT include the '{0}' header.", headers[i]), nameof (headers)); + + if (fields[i] == "from") + containsFrom = true; + } + + if (!containsFrom) + throw new ArgumentException ("The list of headers to sign MUST include the 'From' header.", nameof (headers)); + + DkimSign (options, message, fields); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The message to sign. + /// The headers to sign. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + public void Sign (MimeMessage message, IList headers) + { + Sign (FormatOptions.Default, message, headers); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The formatting options. + /// The message to sign. + /// The list of header fields to sign. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + public void Sign (FormatOptions options, MimeMessage message, IList headers) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (headers == null) + throw new ArgumentNullException (nameof (headers)); + + var fields = new string[headers.Count]; + var containsFrom = false; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i] == HeaderId.Unknown) + throw new ArgumentException ("The list of headers to sign cannot include the 'Unknown' header.", nameof (headers)); + + fields[i] = headers[i].ToHeaderName ().ToLowerInvariant (); + + if (DkimShouldNotInclude.Contains (fields[i])) + throw new ArgumentException (string.Format ("The list of headers to sign SHOULD NOT include the '{0}' header.", headers[i].ToHeaderName ()), nameof (headers)); + + if (headers[i] == HeaderId.From) + containsFrom = true; + } + + if (!containsFrom) + throw new ArgumentException ("The list of headers to sign MUST include the 'From' header.", nameof (headers)); + + DkimSign (options, message, fields); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The message to sign. + /// The headers to sign. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + public void Sign (MimeMessage message, IList headers) + { + Sign (FormatOptions.Default, message, headers); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimSignerBase.cs b/src/MimeKit/Cryptography/DkimSignerBase.cs new file mode 100644 index 0000000..a7f5257 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimSignerBase.cs @@ -0,0 +1,293 @@ +// +// DkimSignerBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +#if ENABLE_NATIVE_DKIM +using System.Security.Cryptography; +#endif + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Signers; + +namespace MimeKit.Cryptography { + /// + /// A base class for DKIM and ARC signers. + /// + /// + /// The base class for and . + /// + public abstract class DkimSignerBase + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// The domain that the signer represents. + /// The selector subdividing the domain. + /// The signature algorithm. + /// + /// is null. + /// -or- + /// is null. + /// + protected DkimSignerBase (string domain, string selector, DkimSignatureAlgorithm algorithm = DkimSignatureAlgorithm.RsaSha256) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + if (selector == null) + throw new ArgumentNullException (nameof (selector)); + + SignatureAlgorithm = algorithm; + Selector = selector; + Domain = domain; + } + + /// + /// Get the domain that the signer represents. + /// + /// + /// Gets the domain that the signer represents. + /// + /// + /// + /// + /// The domain. + public string Domain { + get; private set; + } + + /// + /// Get the selector subdividing the domain. + /// + /// + /// Gets the selector subdividing the domain. + /// + /// + /// + /// + /// The selector. + public string Selector { + get; private set; + } + + /// + /// Get or set the algorithm to use for signing. + /// + /// + /// Gets or sets the algorithm to use for signing. + /// Creates a new . + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be used. + /// + /// + /// + /// + /// The signature algorithm. + public DkimSignatureAlgorithm SignatureAlgorithm { + get; set; + } + + /// + /// Get or set the canonicalization algorithm to use for the message body. + /// + /// + /// Gets or sets the canonicalization algorithm to use for the message body. + /// + /// + /// + /// + /// The canonicalization algorithm. + public DkimCanonicalizationAlgorithm BodyCanonicalizationAlgorithm { + get; set; + } + + /// + /// Get or set the canonicalization algorithm to use for the message headers. + /// + /// + /// Gets or sets the canonicalization algorithm to use for the message headers. + /// + /// + /// + /// + /// The canonicalization algorithm. + public DkimCanonicalizationAlgorithm HeaderCanonicalizationAlgorithm { + get; set; + } + + /// + /// Gets the private key. + /// + /// + /// The private key used for signing. + /// + /// The private key. + protected AsymmetricKeyParameter PrivateKey { + get; set; + } + + internal static AsymmetricKeyParameter LoadPrivateKey (Stream stream) + { + AsymmetricKeyParameter key = null; + + using (var reader = new StreamReader (stream)) { + var pem = new PemReader (reader); + + var keyObject = pem.ReadObject (); + + if (keyObject is AsymmetricCipherKeyPair pair) { + key = pair.Private; + } else if (keyObject is AsymmetricKeyParameter) { + key = (AsymmetricKeyParameter) keyObject; + } + } + + if (key == null || !key.IsPrivate) + throw new FormatException ("Private key not found."); + + return key; + } + + /// + /// Create the digest signing context. + /// + /// + /// Creates a new digest signing context. + /// + /// The digest signer. + /// + /// The is not supported. + /// + internal protected virtual ISigner CreateSigningContext () + { +#if ENABLE_NATIVE_DKIM + return new SystemSecuritySigner (SignatureAlgorithm, PrivateKey.AsAsymmetricAlgorithm ()); +#else + ISigner signer; + + switch (SignatureAlgorithm) { + case DkimSignatureAlgorithm.RsaSha1: + signer = new RsaDigestSigner (new Sha1Digest ()); + break; + case DkimSignatureAlgorithm.RsaSha256: + signer = new RsaDigestSigner (new Sha256Digest ()); + break; + case DkimSignatureAlgorithm.Ed25519Sha256: + signer = new Ed25519DigestSigner (new Sha256Digest ()); + break; + default: + throw new NotSupportedException (string.Format ("{0} is not supported.", SignatureAlgorithm)); + } + + signer.Init (true, PrivateKey); + + return signer; +#endif + } + } + +#if ENABLE_NATIVE_DKIM + class SystemSecuritySigner : ISigner + { + readonly RSACryptoServiceProvider rsa; + readonly HashAlgorithm hash; + readonly string oid; + + public SystemSecuritySigner (DkimSignatureAlgorithm algorithm, AsymmetricAlgorithm key) + { + rsa = key as RSACryptoServiceProvider; + + switch (algorithm) { + case DkimSignatureAlgorithm.RsaSha256: + oid = SecureMimeContext.GetDigestOid (DigestAlgorithm.Sha256); + AlgorithmName = "RSASHA256"; + hash = SHA256.Create (); + break; + default: + oid = SecureMimeContext.GetDigestOid (DigestAlgorithm.Sha1); + AlgorithmName = "RSASHA1"; + hash = SHA1.Create (); + break; + } + } + + public string AlgorithmName { + get; private set; + } + + public void BlockUpdate (byte[] input, int inOff, int length) + { + hash.TransformBlock (input, inOff, length, null, 0); + } + + public byte[] GenerateSignature () + { + hash.TransformFinalBlock (new byte[0], 0, 0); + + return rsa.SignHash (hash.Hash, oid); + } + + public void Init (bool forSigning, ICipherParameters parameters) + { + throw new NotImplementedException (); + } + + public void Reset () + { + hash.Initialize (); + } + + public void Update (byte input) + { + hash.TransformBlock (new byte[] { input }, 0, 1, null, 0); + } + + public bool VerifySignature (byte[] signature) + { + hash.TransformFinalBlock (new byte[0], 0, 0); + + return rsa.VerifyHash (hash.Hash, oid, signature); + } + + public void Dispose () + { + rsa.Dispose (); + } + } +#endif +} diff --git a/src/MimeKit/Cryptography/DkimSimpleBodyFilter.cs b/src/MimeKit/Cryptography/DkimSimpleBodyFilter.cs new file mode 100644 index 0000000..1c1e440 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimSimpleBodyFilter.cs @@ -0,0 +1,140 @@ +// +// DkimSimpleBodyFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// A filter for the DKIM simple body canonicalization. + /// + /// + /// A filter for the DKIM simple body canonicalization. + /// + class DkimSimpleBodyFilter : DkimBodyFilter + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public DkimSimpleBodyFilter () + { + LastWasNewLine = false; + IsEmptyLine = true; + EmptyLines = 0; + } + + unsafe int Filter (byte* inbuf, int length, byte* outbuf) + { + byte* inend = inbuf + length; + byte* outptr = outbuf; + byte* inptr = inbuf; + int count = 0; + + while (inptr < inend) { + if (*inptr == (byte) '\r') { + if (!IsEmptyLine) { + *outptr++ = *inptr; + count++; + } + } else if (*inptr == (byte) '\n') { + if (!IsEmptyLine) { + *outptr++ = *inptr; + LastWasNewLine = true; + IsEmptyLine = true; + EmptyLines = 0; + count++; + } else { + EmptyLines++; + } + } else { + if (EmptyLines > 0) { + // unwind our collection of empty lines + while (EmptyLines > 0) { + *outptr++ = (byte) '\r'; + *outptr++ = (byte) '\n'; + EmptyLines--; + count += 2; + } + } + + LastWasNewLine = false; + IsEmptyLine = false; + + *outptr++ = *inptr; + count++; + } + + inptr++; + } + + return count; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + EnsureOutputSize (length + EmptyLines * 2 + 1, false); + + unsafe { + fixed (byte* inptr = input, outptr = OutputBuffer) { + outputLength = Filter (inptr + startIndex, length, outptr); + } + } + + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + LastWasNewLine = false; + IsEmptyLine = true; + EmptyLines = 0; + + base.Reset (); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimVerifier.cs b/src/MimeKit/Cryptography/DkimVerifier.cs new file mode 100644 index 0000000..f45745e --- /dev/null +++ b/src/MimeKit/Cryptography/DkimVerifier.cs @@ -0,0 +1,308 @@ +// +// DkimVerifier.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; + +using MimeKit.IO; + +namespace MimeKit.Cryptography { + /// + /// A DKIM-Signature verifier. + /// + /// + /// Verifies DomainKeys Identified Mail (DKIM) signatures. + /// + /// + /// + /// + public class DkimVerifier : DkimVerifierBase + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// + /// + /// + /// The public key locator. + /// + /// is null. + /// + public DkimVerifier (IDkimPublicKeyLocator publicKeyLocator) : base (publicKeyLocator) + { + } + + static void ValidateDkimSignatureParameters (IDictionary parameters, out DkimSignatureAlgorithm algorithm, out DkimCanonicalizationAlgorithm headerAlgorithm, + out DkimCanonicalizationAlgorithm bodyAlgorithm, out string d, out string s, out string q, out string[] headers, out string bh, out string b, out int maxLength) + { + bool containsFrom = false; + + if (!parameters.TryGetValue ("v", out string v)) + throw new FormatException ("Malformed DKIM-Signature header: no version parameter detected."); + + if (v != "1") + throw new FormatException (string.Format ("Unrecognized DKIM-Signature version: v={0}", v)); + + ValidateCommonSignatureParameters ("DKIM-Signature", parameters, out algorithm, out headerAlgorithm, out bodyAlgorithm, out d, out s, out q, out headers, out bh, out b, out maxLength); + + for (int i = 0; i < headers.Length; i++) { + if (headers[i].Equals ("from", StringComparison.OrdinalIgnoreCase)) { + containsFrom = true; + break; + } + } + + if (!containsFrom) + throw new FormatException ("Malformed DKIM-Signature header: From header not signed."); + + if (parameters.TryGetValue ("i", out string id)) { + string ident; + int at; + + if ((at = id.LastIndexOf ('@')) == -1) + throw new FormatException ("Malformed DKIM-Signature header: no @ in the AUID value."); + + ident = id.Substring (at + 1); + + if (!ident.Equals (d, StringComparison.OrdinalIgnoreCase) && !ident.EndsWith ("." + d, StringComparison.OrdinalIgnoreCase)) + throw new FormatException ("Invalid DKIM-Signature header: the domain in the AUID does not match the domain parameter."); + } + } + + async Task VerifyAsync (FormatOptions options, MimeMessage message, Header dkimSignature, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (dkimSignature == null) + throw new ArgumentNullException (nameof (dkimSignature)); + + if (dkimSignature.Id != HeaderId.DkimSignature) + throw new ArgumentException ("The signature parameter MUST be a DKIM-Signature header.", nameof (dkimSignature)); + + var parameters = ParseParameterTags (dkimSignature.Id, dkimSignature.Value); + DkimCanonicalizationAlgorithm headerAlgorithm, bodyAlgorithm; + DkimSignatureAlgorithm signatureAlgorithm; + AsymmetricKeyParameter key; + string d, s, q, bh, b; + string[] headers; + int maxLength; + + ValidateDkimSignatureParameters (parameters, out signatureAlgorithm, out headerAlgorithm, out bodyAlgorithm, + out d, out s, out q, out headers, out bh, out b, out maxLength); + + if (!IsEnabled (signatureAlgorithm)) + return false; + + if (doAsync) + key = await PublicKeyLocator.LocatePublicKeyAsync (q, d, s, cancellationToken).ConfigureAwait (false); + else + key = PublicKeyLocator.LocatePublicKey (q, d, s, cancellationToken); + + if ((key is RsaKeyParameters rsa) && rsa.Modulus.BitLength < MinimumRsaKeyLength) + return false; + + options = options.Clone (); + options.NewLineFormat = NewLineFormat.Dos; + + // first check the body hash (if that's invalid, then the entire signature is invalid) + var hash = Convert.ToBase64String (message.HashBody (options, signatureAlgorithm, bodyAlgorithm, maxLength)); + + if (hash != bh) + return false; + + using (var stream = new DkimSignatureStream (CreateVerifyContext (signatureAlgorithm, key))) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + WriteHeaders (options, message, headers, headerAlgorithm, filtered); + + // now include the DKIM-Signature header that we are verifying, + // but only after removing the "b=" signature value. + var header = GetSignedSignatureHeader (dkimSignature); + + switch (headerAlgorithm) { + case DkimCanonicalizationAlgorithm.Relaxed: + WriteHeaderRelaxed (options, filtered, header, true); + break; + default: + WriteHeaderSimple (options, filtered, header, true); + break; + } + + filtered.Flush (); + } + + return stream.VerifySignature (b); + } + } + + /// + /// Verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The formatting options. + /// The message to verify. + /// The DKIM-Signature header. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public bool Verify (FormatOptions options, MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (options, message, dkimSignature, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The formatting options. + /// The message to verify. + /// The DKIM-Signature header. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public Task VerifyAsync (FormatOptions options, MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (options, message, dkimSignature, true, cancellationToken); + } + + /// + /// Verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The message to verify. + /// The DKIM-Signature header. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public bool Verify (MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default (CancellationToken)) + { + return Verify (FormatOptions.Default, message, dkimSignature, cancellationToken); + } + + /// + /// Asynchronously verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The message to verify. + /// The DKIM-Signature header. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + public Task VerifyAsync (MimeMessage message, Header dkimSignature, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (FormatOptions.Default, message, dkimSignature, cancellationToken); + } + } +} diff --git a/src/MimeKit/Cryptography/DkimVerifierBase.cs b/src/MimeKit/Cryptography/DkimVerifierBase.cs new file mode 100644 index 0000000..2528b61 --- /dev/null +++ b/src/MimeKit/Cryptography/DkimVerifierBase.cs @@ -0,0 +1,495 @@ +// +// DkimVerifierBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Globalization; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Signers; + +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// A base class for DKIM and ARC verifiers. + /// + /// + /// The base class for and . + /// + public abstract class DkimVerifierBase + { + int enabledSignatureAlgorithms; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes the . + /// + /// The public key locator. + /// + /// is null. + /// + protected DkimVerifierBase (IDkimPublicKeyLocator publicKeyLocator) + { + if (publicKeyLocator == null) + throw new ArgumentNullException (nameof (publicKeyLocator)); + + PublicKeyLocator = publicKeyLocator; + + Enable (DkimSignatureAlgorithm.Ed25519Sha256); + Enable (DkimSignatureAlgorithm.RsaSha256); + //Enable (DkimSignatureAlgorithm.RsaSha1); + MinimumRsaKeyLength = 1024; + } + + /// + /// Get the public key locator. + /// + /// + /// Gets the public key locator. + /// + /// The public key locator. + protected IDkimPublicKeyLocator PublicKeyLocator { + get; private set; + } + + /// + /// Get or set the minimum allowed RSA key length. + /// + /// + /// Gets the minimum allowed RSA key length. + /// The DKIM specifications specify a single signing algorithm, RSA, + /// and recommend key sizes of 1024 to 2048 bits (but require verification of 512-bit keys). + /// As discussed in US-CERT Vulnerability Note VU#268267, the operational community has + /// recognized that shorter keys compromise the effectiveness of DKIM. While 1024-bit + /// signatures are common, stronger signatures are not. Widely used DNS configuration + /// software places a practical limit on key sizes, because the software only handles a + /// single 256-octet string in a TXT record, and RSA keys significantly longer than 1024 + /// bits don't fit in 256 octets. + /// + public int MinimumRsaKeyLength { + get; set; + } + + /// + /// Enable a DKIM signature algorithm. + /// + /// + /// Enables the specified DKIM signature algorithm. + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be enabled. + /// + /// The DKIM signature algorithm. + public void Enable (DkimSignatureAlgorithm algorithm) + { + enabledSignatureAlgorithms |= 1 << (int) algorithm; + } + + /// + /// Disable a DKIM signature algorithm. + /// + /// + /// Disables the specified DKIM signature algorithm. + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be enabled. + /// + /// The DKIM signature algorithm. + public void Disable (DkimSignatureAlgorithm algorithm) + { + enabledSignatureAlgorithms &= ~(1 << (int) algorithm); + } + + /// + /// Check whether a DKIM signature algorithm is enabled. + /// + /// + /// Determines whether the specified DKIM signature algorithm is enabled. + /// Due to the recognized weakness of the SHA-1 hash algorithm + /// and the wide availability of the SHA-256 hash algorithm (it has been a required + /// part of DKIM since it was originally standardized in 2007), it is recommended + /// that NOT be enabled. + /// + /// true if the specified DKIM signature algorithm is enabled; otherwise, false. + /// The DKIM signature algorithm. + public bool IsEnabled (DkimSignatureAlgorithm algorithm) + { + return (enabledSignatureAlgorithms & (1 << (int) algorithm)) != 0; + } + + static bool IsWhiteSpace (char c) + { + return c == ' ' || c == '\t'; + } + + static bool IsAlpha (char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + internal static Dictionary ParseParameterTags (HeaderId header, string signature) + { + var parameters = new Dictionary (); + var value = new StringBuilder (); + int index = 0; + + while (index < signature.Length) { + while (index < signature.Length && IsWhiteSpace (signature[index])) + index++; + + if (index >= signature.Length) + break; + + if (signature[index] == ';' || !IsAlpha (signature[index])) + throw new FormatException (string.Format ("Malformed {0} value.", header.ToHeaderName ())); + + int startIndex = index++; + + while (index < signature.Length && signature[index] != '=') + index++; + + if (index >= signature.Length) + continue; + + var name = signature.Substring (startIndex, index - startIndex).TrimEnd (); + + // skip over '=' and clear value buffer + value.Length = 0; + index++; + + while (index < signature.Length && signature[index] != ';') { + if (!IsWhiteSpace (signature[index])) + value.Append (signature[index]); + index++; + } + + if (parameters.ContainsKey (name)) + throw new FormatException (string.Format ("Malformed {0} value: duplicate parameter '{1}'.", header.ToHeaderName (), name)); + + parameters.Add (name, value.ToString ()); + + // skip over ';' + index++; + } + + return parameters; + } + + internal static void ValidateCommonParameters (string header, IDictionary parameters, out DkimSignatureAlgorithm algorithm, + out string d, out string s, out string q, out string b) + { + if (!parameters.TryGetValue ("a", out string a)) + throw new FormatException (string.Format ("Malformed {0} header: no signature algorithm parameter detected.", header)); + + switch (a.ToLowerInvariant ()) { + case "ed25519-sha256": algorithm = DkimSignatureAlgorithm.Ed25519Sha256; break; + case "rsa-sha256": algorithm = DkimSignatureAlgorithm.RsaSha256; break; + case "rsa-sha1": algorithm = DkimSignatureAlgorithm.RsaSha1; break; + default: throw new FormatException (string.Format ("Unrecognized {0} algorithm parameter: a={1}", header, a)); + } + + if (!parameters.TryGetValue ("d", out d)) + throw new FormatException (string.Format ("Malformed {0} header: no domain parameter detected.", header)); + + if (d.Length == 0) + throw new FormatException (string.Format ("Malformed {0} header: empty domain parameter detected.", header)); + + if (!parameters.TryGetValue ("s", out s)) + throw new FormatException (string.Format ("Malformed {0} header: no selector parameter detected.", header)); + + if (s.Length == 0) + throw new FormatException (string.Format ("Malformed {0} header: empty selector parameter detected.", header)); + + if (!parameters.TryGetValue ("q", out q)) + q = "dns/txt"; + + if (!parameters.TryGetValue ("b", out b)) + throw new FormatException (string.Format ("Malformed {0} header: no signature parameter detected.", header)); + + if (b.Length == 0) + throw new FormatException (string.Format ("Malformed {0} header: empty signature parameter detected.", header)); + + if (parameters.TryGetValue ("t", out string t)) { + if (!int.TryParse (t, NumberStyles.Integer, CultureInfo.InvariantCulture, out int timestamp) || timestamp < 0) + throw new FormatException (string.Format ("Malformed {0} header: invalid timestamp parameter: t={1}.", header, t)); + } + } + + internal static void ValidateCommonSignatureParameters (string header, IDictionary parameters, out DkimSignatureAlgorithm algorithm, out DkimCanonicalizationAlgorithm headerAlgorithm, + out DkimCanonicalizationAlgorithm bodyAlgorithm, out string d, out string s, out string q, out string[] headers, out string bh, out string b, out int maxLength) + { + ValidateCommonParameters (header, parameters, out algorithm, out d, out s, out q, out b); + + if (parameters.TryGetValue ("l", out string l)) { + if (!int.TryParse (l, NumberStyles.Integer, CultureInfo.InvariantCulture, out maxLength) || maxLength < 0) + throw new FormatException (string.Format ("Malformed {0} header: invalid length parameter: l={1}", header, l)); + } else { + maxLength = -1; + } + + if (parameters.TryGetValue ("c", out string c)) { + var tokens = c.ToLowerInvariant ().Split ('/'); + + if (tokens.Length == 0 || tokens.Length > 2) + throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header, c)); + + switch (tokens[0]) { + case "relaxed": headerAlgorithm = DkimCanonicalizationAlgorithm.Relaxed; break; + case "simple": headerAlgorithm = DkimCanonicalizationAlgorithm.Simple; break; + default: throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header, c)); + } + + if (tokens.Length == 2) { + switch (tokens[1]) { + case "relaxed": bodyAlgorithm = DkimCanonicalizationAlgorithm.Relaxed; break; + case "simple": bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple; break; + default: throw new FormatException (string.Format ("Malformed {0} header: invalid canonicalization parameter: c={1}", header, c)); + } + } else { + bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple; + } + } else { + headerAlgorithm = DkimCanonicalizationAlgorithm.Simple; + bodyAlgorithm = DkimCanonicalizationAlgorithm.Simple; + } + + if (!parameters.TryGetValue ("h", out string h)) + throw new FormatException (string.Format ("Malformed {0} header: no signed header parameter detected.", header)); + + headers = h.Split (':'); + + if (!parameters.TryGetValue ("bh", out bh)) + throw new FormatException (string.Format ("Malformed {0} header: no body hash parameter detected.", header)); + } + + internal static void WriteHeaderRelaxed (FormatOptions options, Stream stream, Header header, bool isDkimSignature) + { + // o Convert all header field names (not the header field values) to + // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". + var name = Encoding.ASCII.GetBytes (header.Field.ToLowerInvariant ()); + var rawValue = header.GetRawValue (options); + int index = 0; + + // o Delete any WSP characters remaining before and after the colon + // separating the header field name from the header field value. The + // colon separator MUST be retained. + stream.Write (name, 0, name.Length); + stream.WriteByte ((byte) ':'); + + // trim leading whitespace... + while (index < rawValue.Length && rawValue[index].IsWhitespace ()) + index++; + + while (index < rawValue.Length) { + int startIndex = index; + + // look for the first non-whitespace character + while (index < rawValue.Length && rawValue[index].IsWhitespace ()) + index++; + + // o Delete all WSP characters at the end of each unfolded header field + // value. + if (index >= rawValue.Length) + break; + + // o Convert all sequences of one or more WSP characters to a single SP + // character. WSP characters here include those before and after a + // line folding boundary. + if (index > startIndex) + stream.WriteByte ((byte) ' '); + + startIndex = index; + + while (index < rawValue.Length && !rawValue[index].IsWhitespace ()) + index++; + + if (index > startIndex) + stream.Write (rawValue, startIndex, index - startIndex); + } + + if (!isDkimSignature) + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + + internal static void WriteHeaderSimple (FormatOptions options, Stream stream, Header header, bool isDkimSignature) + { + var rawValue = header.GetRawValue (options); + int rawLength = rawValue.Length; + + if (isDkimSignature && rawLength > 0) { + if (rawValue[rawLength - 1] == (byte) '\n') { + rawLength--; + + if (rawLength > 0 && rawValue[rawLength - 1] == (byte) '\r') + rawLength--; + } + } + + stream.Write (header.RawField, 0, header.RawField.Length); + stream.Write (Header.Colon, 0, Header.Colon.Length); + stream.Write (rawValue, 0, rawLength); + } + + /// + /// Create the digest signing context. + /// + /// + /// Creates a new digest signing context that uses the specified algorithm. + /// + /// The DKIM signature algorithm. + /// The public key. + /// The digest signer. + internal virtual ISigner CreateVerifyContext (DkimSignatureAlgorithm algorithm, AsymmetricKeyParameter key) + { +#if ENABLE_NATIVE_DKIM + return new SystemSecuritySigner (algorithm, key.AsAsymmetricAlgorithm ()); +#else + ISigner signer; + + switch (algorithm) { + case DkimSignatureAlgorithm.RsaSha1: + signer = new RsaDigestSigner (new Sha1Digest ()); + break; + case DkimSignatureAlgorithm.RsaSha256: + signer = new RsaDigestSigner (new Sha256Digest ()); + break; + case DkimSignatureAlgorithm.Ed25519Sha256: + signer = new Ed25519DigestSigner (new Sha256Digest ()); + break; + default: + throw new NotSupportedException (string.Format ("{0} is not supported.", algorithm)); + } + + signer.Init (key.IsPrivate, key); + + return signer; +#endif + } + + internal static void WriteHeaders (FormatOptions options, MimeMessage message, IList fields, DkimCanonicalizationAlgorithm headerCanonicalizationAlgorithm, Stream stream) + { + var counts = new Dictionary (StringComparer.Ordinal); + + for (int i = 0; i < fields.Count; i++) { + var headers = fields[i].StartsWith ("Content-", StringComparison.OrdinalIgnoreCase) ? message.Body.Headers : message.Headers; + var name = fields[i].ToLowerInvariant (); + int index, count, n = 0; + + if (!counts.TryGetValue (name, out count)) + count = 0; + + // Note: signers choosing to sign an existing header field that occurs more + // than once in the message (such as Received) MUST sign the physically last + // instance of that header field in the header block. Signers wishing to sign + // multiple instances of such a header field MUST include the header field + // name multiple times in the list of header fields and MUST sign such header + // fields in order from the bottom of the header field block to the top. + index = headers.LastIndexOf (name); + + // find the n'th header with this name + while (n < count && --index >= 0) { + if (headers[index].Field.Equals (name, StringComparison.OrdinalIgnoreCase)) + n++; + } + + if (index < 0) + continue; + + var header = headers[index]; + + switch (headerCanonicalizationAlgorithm) { + case DkimCanonicalizationAlgorithm.Relaxed: + WriteHeaderRelaxed (options, stream, header, false); + break; + default: + WriteHeaderSimple (options, stream, header, false); + break; + } + + counts[name] = ++count; + } + } + + internal static Header GetSignedSignatureHeader (Header header) + { + // modify the raw DKIM-Signature header value by chopping off the signature value after the "b=" + var rawValue = (byte[]) header.RawValue.Clone (); + int length = 0, index = 0; + + do { + while (index < rawValue.Length && rawValue[index].IsWhitespace ()) + index++; + + if (index + 2 < rawValue.Length) { + var param = (char) rawValue[index++]; + + while (index < rawValue.Length && rawValue[index].IsWhitespace ()) + index++; + + if (index < rawValue.Length && rawValue[index] == (byte) '=' && param == 'b') { + length = ++index; + + while (index < rawValue.Length && rawValue[index] != (byte) ';') + index++; + + if (index == rawValue.Length && rawValue[index - 1] == (byte) '\n') { + index--; + + if (rawValue[index - 1] == (byte) '\r') + index--; + } + + break; + } + } + + while (index < rawValue.Length && rawValue[index] != (byte) ';') + index++; + + if (index < rawValue.Length) + index++; + } while (index < rawValue.Length); + + if (index == rawValue.Length) + throw new FormatException (string.Format ("Malformed {0} header: missing signature parameter.", header.Id.ToHeaderName ())); + + while (index < rawValue.Length) + rawValue[length++] = rawValue[index++]; + + Array.Resize (ref rawValue, length); + + return new Header (header.Options, header.RawField, rawValue, false); + } + } +} diff --git a/src/MimeKit/Cryptography/Ed25519DigestSigner.cs b/src/MimeKit/Cryptography/Ed25519DigestSigner.cs new file mode 100644 index 0000000..5f00ab5 --- /dev/null +++ b/src/MimeKit/Cryptography/Ed25519DigestSigner.cs @@ -0,0 +1,112 @@ +// +// Ed25519DigestSigner.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Math.EC.Rfc8032; +using Org.BouncyCastle.Crypto.Parameters; + +namespace MimeKit.Cryptography { + class Ed25519DigestSigner : ISigner + { + Ed25519PrivateKeyParameters privateKey; + Ed25519PublicKeyParameters publicKey; + readonly IDigest digest; + + public Ed25519DigestSigner (IDigest digest) + { + this.digest = digest; + } + + public string AlgorithmName { + get { return digest.AlgorithmName + "withEd25519"; } + } + + public void Init (bool forSigning, ICipherParameters parameters) + { + if (forSigning) { + privateKey = (Ed25519PrivateKeyParameters) parameters; + publicKey = privateKey.GeneratePublicKey (); + } else { + publicKey = (Ed25519PublicKeyParameters) parameters; + privateKey = null; + } + + Reset (); + } + + public void Update (byte input) + { + digest.Update (input); + } + + public void BlockUpdate (byte[] input, int inOff, int length) + { + digest.BlockUpdate (input, inOff, length); + } + + public byte[] GenerateSignature () + { + if (privateKey == null) + throw new InvalidOperationException ("Ed25519DigestSigner not initialised for signature generation."); + + var hash = new byte[digest.GetDigestSize ()]; + digest.DoFinal (hash, 0); + + var signature = new byte[Ed25519PrivateKeyParameters.SignatureSize]; + privateKey.Sign (Ed25519.Algorithm.Ed25519, publicKey, null, hash, 0, hash.Length, signature, 0); + + Reset (); + + return signature; + } + + public bool VerifySignature (byte[] signature) + { + if (privateKey != null) + throw new InvalidOperationException ("Ed25519DigestSigner not initialised for verification"); + + if (Ed25519.SignatureSize != signature.Length) + return false; + + byte[] hash = new byte[digest.GetDigestSize ()]; + digest.DoFinal (hash, 0); + + var pk = publicKey.GetEncoded (); + var result = Ed25519.Verify (signature, 0, pk, 0, hash, 0, hash.Length); + + Reset (); + + return result; + } + + public void Reset () + { + digest.Reset (); + } + } +} diff --git a/src/MimeKit/Cryptography/EncryptionAlgorithm.cs b/src/MimeKit/Cryptography/EncryptionAlgorithm.cs new file mode 100644 index 0000000..cc092f2 --- /dev/null +++ b/src/MimeKit/Cryptography/EncryptionAlgorithm.cs @@ -0,0 +1,142 @@ +// +// EncryptionAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// Encryption algorithms supported by S/MIME and OpenPGP. + /// + /// + /// Represents the available encryption algorithms for use with S/MIME and OpenPGP. + /// RC-2/40 was required by all S/MIME v2 implementations. However, since the + /// mid-to-late 1990's, RC-2/40 has been considered to be extremely weak and starting with + /// S/MIME v3.0 (published in 1999), all S/MIME implementations are required to implement + /// support for Triple-DES (aka 3DES) and should no longer encrypt using RC-2/40 unless + /// explicitly requested to do so by the user. + /// These days, most S/MIME implementations support the AES-128 and AES-256 + /// algorithms which are the recommended algorithms specified in S/MIME v3.2 and + /// should be preferred over the use of Triple-DES unless the client capabilities + /// of one or more of the recipients is unknown (or only supports Triple-DES). + /// + public enum EncryptionAlgorithm { + /// + /// The AES 128-bit encryption algorithm. + /// + Aes128, + + /// + /// The AES 192-bit encryption algorithm. + /// + Aes192, + + /// + /// The AES 256-bit encryption algorithm. + /// + Aes256, + + /// + /// The Camellia 128-bit encryption algorithm. + /// + Camellia128, + + /// + /// The Camellia 192-bit encryption algorithm. + /// + Camellia192, + + /// + /// The Camellia 256-bit encryption algorithm. + /// + Camellia256, + + /// + /// The Cast-5 128-bit encryption algorithm. + /// + Cast5, + + /// + /// The DES 56-bit encryption algorithm. + /// + /// + /// This is extremely weak encryption and should not be used + /// without consent from the user. + /// + Des, + + /// + /// The Triple-DES encryption algorithm. + /// + /// + /// This is the weakest recommended encryption algorithm for use + /// starting with S/MIME v3 and should only be used as a fallback + /// if it is unknown what encryption algorithms are supported by + /// the recipient's mail client. + /// + TripleDes, + + /// + /// The IDEA 128-bit encryption algorithm. + /// + Idea, + + /// + /// The Blowfish encryption algorithm. + /// + Blowfish, + + /// + /// The Twofish encryption algorithm. + /// + Twofish, + + /// + /// The RC2 40-bit encryption algorithm (S/MIME only). + /// + /// + /// This is extremely weak encryption and should not be used + /// without consent from the user. + /// + RC240, + + /// + /// The RC2 64-bit encryption algorithm (S/MIME only). + /// + /// + /// This is very weak encryption and should not be used + /// without consent from the user. + /// + RC264, + + /// + /// The RC2 128-bit encryption algorithm (S/MIME only). + /// + RC2128, + + /// + /// The SEED 128-bit encryption algorithm (S/MIME only). + /// + Seed + } +} diff --git a/src/MimeKit/Cryptography/GnuPGContext.cs b/src/MimeKit/Cryptography/GnuPGContext.cs new file mode 100644 index 0000000..efbe928 --- /dev/null +++ b/src/MimeKit/Cryptography/GnuPGContext.cs @@ -0,0 +1,268 @@ +// +// GnuPGContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +namespace MimeKit.Cryptography { + /// + /// A that uses the GnuPG keyrings. + /// + /// + /// A that uses the GnuPG keyrings. + /// + public abstract class GnuPGContext : OpenPgpContext + { + static readonly Dictionary EncryptionAlgorithms; + //static readonly Dictionary PublicKeyAlgorithms; + static readonly Dictionary DigestAlgorithms; + static readonly char[] Whitespace = { ' ', '\t' }; + static readonly string PublicKeyRing; + static readonly string SecretKeyRing; + static readonly string Configuration; + + static GnuPGContext () + { + var gnupg = Environment.GetEnvironmentVariable ("GNUPGHOME"); + + if (gnupg == null) { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + if (Path.DirectorySeparatorChar == '\\') { + var appData = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + gnupg = Path.Combine (appData, "gnupg"); + } else { + var home = Environment.GetFolderPath (Environment.SpecialFolder.Personal); + gnupg = Path.Combine (home, ".gnupg"); + } +#else + gnupg = ".gnupg"; +#endif + } + + PublicKeyRing = Path.Combine (gnupg, "pubring.gpg"); + SecretKeyRing = Path.Combine (gnupg, "secring.gpg"); + Configuration = Path.Combine (gnupg, "gpg.conf"); + + EncryptionAlgorithms = new Dictionary (StringComparer.Ordinal) { + { "AES", EncryptionAlgorithm.Aes128 }, + { "AES128", EncryptionAlgorithm.Aes128 }, + { "AES192", EncryptionAlgorithm.Aes192 }, + { "AES256", EncryptionAlgorithm.Aes256 }, + { "BLOWFISH", EncryptionAlgorithm.Blowfish }, + { "CAMELLIA128", EncryptionAlgorithm.Camellia128 }, + { "CAMELLIA192", EncryptionAlgorithm.Camellia192 }, + { "CAMELLIA256", EncryptionAlgorithm.Camellia256 }, + { "CAST5", EncryptionAlgorithm.Cast5 }, + { "IDEA", EncryptionAlgorithm.Idea }, + { "3DES", EncryptionAlgorithm.TripleDes }, + { "TWOFISH", EncryptionAlgorithm.Twofish } + }; + + //PublicKeyAlgorithms = new Dictionary { + // { "DSA", PublicKeyAlgorithm.Dsa }, + // { "ECDH", PublicKeyAlgorithm.EllipticCurve }, + // { "ECDSA", PublicKeyAlgorithm.EllipticCurveDsa }, + // { "EDDSA", PublicKeyAlgorithm.EdwardsCurveDsa }, + // { "ELG", PublicKeyAlgorithm.ElGamalGeneral }, + // { "RSA", PublicKeyAlgorithm.RsaGeneral } + //}; + + DigestAlgorithms = new Dictionary (StringComparer.Ordinal) { + { "RIPEMD160", DigestAlgorithm.RipeMD160 }, + { "SHA1", DigestAlgorithm.Sha1 }, + { "SHA224", DigestAlgorithm.Sha224 }, + { "SHA256", DigestAlgorithm.Sha256 }, + { "SHA384", DigestAlgorithm.Sha384 }, + { "SHA512", DigestAlgorithm.Sha512 } + }; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + protected GnuPGContext () : base (PublicKeyRing, SecretKeyRing) + { + LoadConfiguration (); + + foreach (var algorithm in EncryptionAlgorithmRank) + Enable (algorithm); + + foreach (var algorithm in DigestAlgorithmRank) + Enable (algorithm); + } + + void UpdateKeyServer (string value) + { + if (string.IsNullOrEmpty (value)) { + KeyServer = null; + return; + } + + if (!Uri.IsWellFormedUriString (value, UriKind.Absolute)) + return; + + KeyServer = new Uri (value, UriKind.Absolute); + } + + void UpdateKeyServerOptions (string value) + { + if (string.IsNullOrEmpty (value)) + return; + + var options = value.Split (Whitespace, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < options.Length; i++) { + switch (options[i]) { + case "auto-key-retrieve": + AutoKeyRetrieve = true; + break; + } + } + } + + static EncryptionAlgorithm[] ParseEncryptionAlgorithms (string value) + { + var names = value.Split (Whitespace, StringSplitOptions.RemoveEmptyEntries); + var algorithms = new List (); + var seen = new HashSet (); + + for (int i = 0; i < names.Length; i++) { + var name = names[i].ToUpperInvariant (); + EncryptionAlgorithm algorithm; + + if (EncryptionAlgorithms.TryGetValue (name, out algorithm) && seen.Add (algorithm)) + algorithms.Add (algorithm); + } + + if (!seen.Contains (EncryptionAlgorithm.TripleDes)) + algorithms.Add (EncryptionAlgorithm.TripleDes); + + return algorithms.ToArray (); + } + + //static PublicKeyAlgorithm[] ParsePublicKeyAlgorithms (string value) + //{ + // var names = value.Split (new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + // var algorithms = new List (); + // var seen = new HashSet (); + + // for (int i = 0; i < names.Length; i++) { + // var name = names[i].ToUpperInvariant (); + // PublicKeyAlgorithm algorithm; + + // if (PublicKeyAlgorithms.TryGetValue (name, out algorithm) && seen.Add (algorithm)) + // algorithms.Add (algorithm); + // } + + // if (!seen.Contains (PublicKeyAlgorithm.Dsa)) + // seen.Add (PublicKeyAlgorithm.Dsa); + + // return algorithms.ToArray (); + //} + + static DigestAlgorithm[] ParseDigestAlgorithms (string value) + { + var names = value.Split (Whitespace, StringSplitOptions.RemoveEmptyEntries); + var algorithms = new List (); + var seen = new HashSet (); + + for (int i = 0; i < names.Length; i++) { + var name = names[i].ToUpperInvariant (); + DigestAlgorithm algorithm; + + if (DigestAlgorithms.TryGetValue (name, out algorithm) && seen.Add (algorithm)) + algorithms.Add (algorithm); + } + + if (!seen.Contains (DigestAlgorithm.Sha1)) + algorithms.Add (DigestAlgorithm.Sha1); + + return algorithms.ToArray (); + } + + void UpdatePersonalCipherPreferences (string value) + { + EncryptionAlgorithmRank = ParseEncryptionAlgorithms (value); + } + + void UpdatePersonalDigestPreferences (string value) + { + DigestAlgorithmRank = ParseDigestAlgorithms (value); + } + + void LoadConfiguration () + { + if (!File.Exists (Configuration)) + return; + + using (var reader = File.OpenText (Configuration)) { + string line; + + while ((line = reader.ReadLine ()) != null) { + int startIndex = 0; + + while (startIndex < line.Length && char.IsWhiteSpace (line[startIndex])) + startIndex++; + + if (startIndex == line.Length || line[startIndex] == '#') + continue; + + int endIndex = startIndex; + while (endIndex < line.Length && !char.IsWhiteSpace (line[endIndex])) + endIndex++; + + var option = line.Substring (startIndex, endIndex - startIndex); + string value; + + if (endIndex < line.Length) + value = line.Substring (endIndex + 1).Trim (); + else + value = null; + + switch (option) { + case "keyserver": + UpdateKeyServer (value); + break; + case "keyserver-options": + UpdateKeyServerOptions (value); + break; + case "personal-cipher-preferences": + UpdatePersonalCipherPreferences (value); + break; + case "personal-digest-preferences": + UpdatePersonalDigestPreferences (value); + break; + //case "personal-compress-preferences": + // break; + } + } + } + } + } +} diff --git a/src/MimeKit/Cryptography/IDigitalCertificate.cs b/src/MimeKit/Cryptography/IDigitalCertificate.cs new file mode 100644 index 0000000..45e6fac --- /dev/null +++ b/src/MimeKit/Cryptography/IDigitalCertificate.cs @@ -0,0 +1,92 @@ +// +// IDigitalSigner.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Cryptography { + /// + /// An interface for a digital certificate. + /// + /// + /// An interface for a digital certificate. + /// + public interface IDigitalCertificate + { + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// The public key algorithm. + PublicKeyAlgorithm PublicKeyAlgorithm { get; } + + /// + /// Gets the date that the certificate was created. + /// + /// + /// Gets the date that the certificate was created. + /// + /// The creation date. + DateTime CreationDate { get; } + + /// + /// Gets the expiration date of the certificate. + /// + /// + /// Gets the expiration date of the certificate. + /// + /// The expiration date. + DateTime ExpirationDate { get; } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// Gets the fingerprint of the certificate. + /// + /// The fingerprint. + string Fingerprint { get; } + + /// + /// Gets the email address of the owner of the certificate. + /// + /// + /// Gets the email address of the owner of the certificate. + /// + /// The email address. + string Email { get; } + + /// + /// Gets the name of the owner of the certificate. + /// + /// + /// Gets the name of the owner of the certificate. + /// + /// The name of the owner. + string Name { get; } + } +} diff --git a/src/MimeKit/Cryptography/IDigitalSignature.cs b/src/MimeKit/Cryptography/IDigitalSignature.cs new file mode 100644 index 0000000..aa26eb0 --- /dev/null +++ b/src/MimeKit/Cryptography/IDigitalSignature.cs @@ -0,0 +1,99 @@ +// +// DigitalSignature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Cryptography { + /// + /// An interface for a digital signature. + /// + /// + /// An interface for a digital signature. + /// + public interface IDigitalSignature + { + /// + /// Gets certificate used by the signer. + /// + /// + /// Gets certificate used by the signer. + /// + /// The signer's certificate. + IDigitalCertificate SignerCertificate { get; } + + /// + /// Gets the public key algorithm used for the signature. + /// + /// + /// Gets the public key algorithm used for the signature. + /// + /// The public key algorithm. + PublicKeyAlgorithm PublicKeyAlgorithm { get; } + + /// + /// Gets the digest algorithm used for the signature. + /// + /// + /// Gets the digest algorithm used for the signature. + /// + /// The digest algorithm. + DigestAlgorithm DigestAlgorithm { get; } + + /// + /// Gets the creation date of the digital signature. + /// + /// + /// Gets the creation date of the digital signature. + /// + /// The creation date. + DateTime CreationDate { get; } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + bool Verify (); + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if only the signature itself should be verified; otherwise, both the signature and the certificate chain are validated. + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + bool Verify (bool verifySignatureOnly); + } +} diff --git a/src/MimeKit/Cryptography/IDkimPublicKeyLocator.cs b/src/MimeKit/Cryptography/IDkimPublicKeyLocator.cs new file mode 100644 index 0000000..c729dbf --- /dev/null +++ b/src/MimeKit/Cryptography/IDkimPublicKeyLocator.cs @@ -0,0 +1,96 @@ +// +// IDkimPublicKeyLocator.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; +using System.Threading.Tasks; + +using Org.BouncyCastle.Crypto; + +namespace MimeKit.Cryptography { + /// + /// An interface for a service which locates and retrieves DKIM public keys (probably via DNS). + /// + /// + /// An interface for a service which locates and retrieves DKIM public keys (probably via DNS). + /// Since MimeKit itself does not implement DNS, it is up to the client to implement public key lookups + /// via DNS. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public interface IDkimPublicKeyLocator + { + /// + /// Locate and retrieve the public key for the given domain and selector. + /// + /// + /// Locates and retrieves the public key for the given domain and selector. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The public key. + /// A colon-separated list of query methods used to retrieve the public key. The default is "dns/txt". + /// The domain. + /// The selector. + /// The cancellation token. + AsymmetricKeyParameter LocatePublicKey (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously locate and retrieve the public key for the given domain and selector. + /// + /// + /// Locates and retrieves the public key for the given domain and selector. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The public key. + /// A colon-separated list of query methods used to retrieve the public key. The default is "dns/txt". + /// The domain. + /// The selector. + /// The cancellation token. + Task LocatePublicKeyAsync (string methods, string domain, string selector, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MimeKit/Cryptography/IX509CertificateDatabase.cs b/src/MimeKit/Cryptography/IX509CertificateDatabase.cs new file mode 100644 index 0000000..9be106a --- /dev/null +++ b/src/MimeKit/Cryptography/IX509CertificateDatabase.cs @@ -0,0 +1,196 @@ +// +// IX509CertificateDatabase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// An interface for an X.509 Certificate database. + /// + /// + /// An X.509 certificate database is used for storing certificates, metdata related to the certificates + /// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs), + /// and private keys. + /// + public interface IX509CertificateDatabase : IX509Store, IDisposable + { + /// + /// Find the specified certificate. + /// + /// + /// Searches the database for the specified certificate, returning the matching + /// record with the desired fields populated. + /// + /// The matching record if found; otherwise null. + /// The certificate. + /// The desired fields. + X509CertificateRecord Find (X509Certificate certificate, X509CertificateRecordFields fields); + + /// + /// Finds the certificates matching the specified selector. + /// + /// + /// Searches the database for certificates matching the selector, returning all + /// matching certificates. + /// + /// The matching certificates. + /// The match selector or null to return all certificates. + IEnumerable FindCertificates (IX509Selector selector); + + /// + /// Finds the private keys matching the specified selector. + /// + /// + /// Searches the database for certificate records matching the selector, returning the + /// private keys for each matching record. + /// + /// The matching certificates. + /// The match selector or null to return all private keys. + IEnumerable FindPrivateKeys (IX509Selector selector); + + /// + /// Finds the certificate records for the specified mailbox. + /// + /// + /// Searches the database for certificates matching the specified mailbox that are valid + /// for the date and time specified, returning all matching records populated with the + /// desired fields. + /// + /// The matching certificate records populated with the desired fields. + /// The mailbox. + /// The date and time. + /// true if a private key is required. + /// The desired fields. + IEnumerable Find (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields); + + /// + /// Finds the certificate records matching the specified selector. + /// + /// + /// Searches the database for certificate records matching the selector, returning all + /// of the matching records populated with the desired fields. + /// + /// The matching certificate records populated with the desired fields. + /// The match selector or null to match all certificates. + /// true if only trusted anchor certificates should be returned. + /// The desired fields. + IEnumerable Find (IX509Selector selector, bool trustedAnchorsOnly, X509CertificateRecordFields fields); + + /// + /// Add the specified certificate record. + /// + /// + /// Adds the specified certificate record to the database. + /// + /// The certificate record. + void Add (X509CertificateRecord record); + + /// + /// Remove the specified certificate record. + /// + /// + /// Removes the specified certificate record from the database. + /// + /// The certificate record. + void Remove (X509CertificateRecord record); + + /// + /// Update the specified certificate record. + /// + /// + /// Updates the specified fields of the record in the database. + /// + /// The certificate record. + /// The fields to update. + void Update (X509CertificateRecord record, X509CertificateRecordFields fields); + + /// + /// Finds the CRL records for the specified issuer. + /// + /// + /// Searches the database for CRL records matching the specified issuer, returning + /// all matching records populated with the desired fields. + /// + /// The matching CRL records populated with the desired fields. + /// The issuer. + /// The desired fields. + IEnumerable Find (X509Name issuer, X509CrlRecordFields fields); + + /// + /// Finds the specified certificate revocation list. + /// + /// + /// Searches the database for the specified CRL, returning the matching record with + /// the desired fields populated. + /// + /// The matching record if found; otherwise null. + /// The certificate revocation list. + /// The desired fields. + X509CrlRecord Find (X509Crl crl, X509CrlRecordFields fields); + + /// + /// Add the specified CRL record. + /// + /// + /// Adds the specified CRL record to the database. + /// + /// The CRL record. + void Add (X509CrlRecord record); + + /// + /// Remove the specified CRL record. + /// + /// + /// Removes the specified CRL record from the database. + /// + /// The CRL record. + void Remove (X509CrlRecord record); + + /// + /// Update the specified CRL record. + /// + /// + /// Updates the specified fields of the record in the database. + /// + /// The CRL record. + void Update (X509CrlRecord record); + + /// + /// Gets a certificate revocation list store. + /// + /// + /// Gets a certificate revocation list store. + /// + /// A certificate recovation list store. + IX509Store GetCrlStore (); + } +} diff --git a/src/MimeKit/Cryptography/LdapUri.cs b/src/MimeKit/Cryptography/LdapUri.cs new file mode 100644 index 0000000..0533814 --- /dev/null +++ b/src/MimeKit/Cryptography/LdapUri.cs @@ -0,0 +1,161 @@ +// +// LdapUri.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.DirectoryServices.Protocols; + +namespace MimeKit.Cryptography +{ + class LdapUri + { + static readonly char[] EndOfHostPort = { ':', '/' }; + static readonly char[] Comma = { ',' }; + + const string DefaultFilter = "(objectClass=*)"; + + public string Scheme { get; private set; } + public string Host { get; private set; } + public int Port { get; private set; } + public string DistinguishedName { get; private set; } + public string[] Attributes { get; private set; } + public SearchScope Scope { get; private set; } + public string Filter { get; private set; } + public string[] Extensions { get; private set; } + + public LdapUri (string scheme) + { + Scheme = scheme; + Host = string.Empty; + DistinguishedName = string.Empty; + Attributes = new string[] { "*" }; + Scope = SearchScope.Base; + Filter = DefaultFilter; + } + + public static bool TryParse (string location, out LdapUri uri) + { + // https://www.ietf.org/rfc/rfc2255.txt + int startIndex, index; + string value; + + uri = null; + + // parse the scheme + if ((index = location.IndexOf (':')) == -1 || index + 2 >= location.Length || location[index + 1] != '/' || location[index + 2] != '/') + return false; + + uri = new LdapUri (location.Substring (0, index)); + + if ((startIndex = index + 3) >= location.Length) + return true; + + // parse the hostname + if ((index = location.IndexOfAny (EndOfHostPort, startIndex)) == -1) + index = location.Length; + + uri.Host = location.Substring (startIndex, index - startIndex); + + if (index < location.Length && location[index] == ':') { + if ((startIndex = index + 1) >= location.Length) + return false; + + // parse the port + if ((index = location.IndexOf ('/', startIndex)) == -1) + index = location.Length; + + value = location.Substring (startIndex, index - startIndex); + if (!ushort.TryParse (value, out ushort port)) + return false; + + uri.Port = port; + } + + if ((startIndex = index + 1) >= location.Length) + return true; + + // parse the distinguished-name + if ((index = location.IndexOf ('?')) == -1) + index = location.Length; + + value = location.Substring (startIndex, index - startIndex); + uri.DistinguishedName = Uri.UnescapeDataString (value); + + if ((startIndex = index + 1) >= location.Length) + return true; + + // parse the attributes + if ((index = location.IndexOf ('?', startIndex)) == -1) + index = location.Length; + + if (index > startIndex) { + value = location.Substring (startIndex, index - startIndex); + uri.Attributes = value.Split (Comma, StringSplitOptions.RemoveEmptyEntries).Select (attr => Uri.UnescapeDataString (attr)).ToArray (); + } + + if ((startIndex = index + 1) >= location.Length) + return true; + + // parse the scope + if ((index = location.IndexOf ('?', startIndex)) == -1) + index = location.Length; + + value = location.Substring (startIndex, index - startIndex); + switch (value.ToLowerInvariant ()) { + case "base": uri.Scope = SearchScope.Base; break; + case "one": uri.Scope = SearchScope.OneLevel; break; + case "sub": uri.Scope = SearchScope.Subtree; break; + default: + // Note: Assuming that Example #7 in rfc5522 is correctly formed, then + // we need to backtrack and parse this as the filter instead. + index = startIndex - 1; + break; + } + + if ((startIndex = index + 1) >= location.Length) + return true; + + // parse the filter + if ((index = location.IndexOf ('?', startIndex)) == -1) + index = location.Length; + + if (index > startIndex) { + value = location.Substring (startIndex, index - startIndex); + uri.Filter = Uri.UnescapeDataString (value); + } + + if ((startIndex = index + 1) >= location.Length) + return true; + + index = location.Length; + + value = location.Substring (startIndex, index - startIndex); + uri.Extensions = value.Split (Comma, StringSplitOptions.RemoveEmptyEntries).Select (extn => Uri.UnescapeDataString (extn)).ToArray (); + + return true; + } + } +} diff --git a/src/MimeKit/Cryptography/MD5.cs b/src/MimeKit/Cryptography/MD5.cs new file mode 100644 index 0000000..10719be --- /dev/null +++ b/src/MimeKit/Cryptography/MD5.cs @@ -0,0 +1,749 @@ +// +// System.Security.Cryptography.MD5CryptoServiceProvider.cs +// +// Authors: +// Matthew S. Ford (Matthew.S.Ford@Rose-Hulman.Edu) +// Sebastien Pouliot (sebastien@ximian.com) +// +// Copyright 2001 by Matthew S. Ford. +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; + +namespace MimeKit.Cryptography { + /// + /// The MD5 hash algorithm. + /// + /// + /// This class is only here for for portability reasons and should + /// not really be considered part of the MimeKit API. + /// + public sealed class MD5 : IDisposable + { + const int BLOCK_SIZE_BYTES = 64; + + static readonly uint[] K = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 + }; + + byte[] hashValue; + byte[] queuedData; // Used to store data when passed less than a block worth. + int queuedCount; // Counts how much data we have stored that still needs processed. + uint[] _H, buff; + bool disposed; + ulong count; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new instance of an MD5 hash algorithm context. + /// + MD5 () + { + queuedData = new byte [BLOCK_SIZE_BYTES]; + buff = new uint[16]; + _H = new uint[4]; + + Initialize (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new instance of an MD5 hash algorithm context. + /// + public static MD5 Create () + { + return new MD5 (); + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~MD5 () + { + Dispose (false); + } + + /// + /// Gets the value of the computed hash code. + /// + /// + /// Gets the value of the computed hash code. + /// + /// The computed hash code. + /// + /// No hash value has been computed. + /// + public byte[] Hash { + get { + if (hashValue == null) + throw new InvalidOperationException ("No hash value computed."); + + return hashValue; + } + } + + void HashCore (byte[] block, int offset, int size) + { + int i; + + if (queuedCount != 0) { + if (size < (BLOCK_SIZE_BYTES - queuedCount)) { + Buffer.BlockCopy (block, offset, queuedData, queuedCount, size); + queuedCount += size; + return; + } + + i = (BLOCK_SIZE_BYTES - queuedCount); + Buffer.BlockCopy (block, offset, queuedData, queuedCount, i); + ProcessBlock (queuedData, 0); + queuedCount = 0; + offset += i; + size -= i; + } + + for (i = 0; i < size - size % BLOCK_SIZE_BYTES; i += BLOCK_SIZE_BYTES) + ProcessBlock (block, offset + i); + + if (size % BLOCK_SIZE_BYTES != 0) { + Buffer.BlockCopy (block, size - size % BLOCK_SIZE_BYTES + offset, queuedData, 0, size % BLOCK_SIZE_BYTES); + queuedCount = size % BLOCK_SIZE_BYTES; + } + } + + byte[] HashFinal () + { + byte[] hash = new byte[16]; + int i, j; + + ProcessFinalBlock (queuedData, 0, queuedCount); + + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) { + hash[i * 4 + j] = (byte)(_H[i] >> j * 8); + } + } + + return hash; + } + + /// + /// Initializes (or re-initializes) the MD5 hash algorithm context. + /// + /// + /// Initializes (or re-initializes) the MD5 hash algorithm context. + /// + public void Initialize () + { + queuedCount = 0; + count = 0; + + _H[0] = 0x67452301; + _H[1] = 0xefcdab89; + _H[2] = 0x98badcfe; + _H[3] = 0x10325476; + } + + void ProcessBlock (byte[] block, int offset) + { + uint a, b, c, d; + int i; + + count += BLOCK_SIZE_BYTES; + + for (i = 0; i < 16; i++) { + buff[i] = (uint)(block[offset + 4 * i]) + | (((uint)(block[offset + 4 * i + 1])) << 8) + | (((uint)(block[offset + 4 * i + 2])) << 16) + | (((uint)(block[offset + 4 * i + 3])) << 24); + } + + a = _H[0]; + b = _H[1]; + c = _H[2]; + d = _H[3]; + + // This function was unrolled because it seems to be doubling our performance with current compiler/VM. + // Possibly roll up if this changes. + + // ---- Round 1 -------- + + a += (((c ^ d) & b) ^ d) + (uint) K[0] + buff[0]; + a = (a << 7) | (a >> 25); + a += b; + + d += (((b ^ c) & a) ^ c) + (uint) K[1] + buff[1]; + d = (d << 12) | (d >> 20); + d += a; + + c += (((a ^ b) & d) ^ b) + (uint) K[2] + buff[2]; + c = (c << 17) | (c >> 15); + c += d; + + b += (((d ^ a) & c) ^ a) + (uint) K[3] + buff[3]; + b = (b << 22) | (b >> 10); + b += c; + + a += (((c ^ d) & b) ^ d) + (uint) K[4] + buff[4]; + a = (a << 7) | (a >> 25); + a += b; + + d += (((b ^ c) & a) ^ c) + (uint) K[5] + buff[5]; + d = (d << 12) | (d >> 20); + d += a; + + c += (((a ^ b) & d) ^ b) + (uint) K[6] + buff[6]; + c = (c << 17) | (c >> 15); + c += d; + + b += (((d ^ a) & c) ^ a) + (uint) K[7] + buff[7]; + b = (b << 22) | (b >> 10); + b += c; + + a += (((c ^ d) & b) ^ d) + (uint) K[8] + buff[8]; + a = (a << 7) | (a >> 25); + a += b; + + d += (((b ^ c) & a) ^ c) + (uint) K[9] + buff[9]; + d = (d << 12) | (d >> 20); + d += a; + + c += (((a ^ b) & d) ^ b) + (uint) K[10] + buff[10]; + c = (c << 17) | (c >> 15); + c += d; + + b += (((d ^ a) & c) ^ a) + (uint) K[11] + buff[11]; + b = (b << 22) | (b >> 10); + b += c; + + a += (((c ^ d) & b) ^ d) + (uint) K[12] + buff[12]; + a = (a << 7) | (a >> 25); + a += b; + + d += (((b ^ c) & a) ^ c) + (uint) K[13] + buff[13]; + d = (d << 12) | (d >> 20); + d += a; + + c += (((a ^ b) & d) ^ b) + (uint) K[14] + buff[14]; + c = (c << 17) | (c >> 15); + c += d; + + b += (((d ^ a) & c) ^ a) + (uint) K[15] + buff[15]; + b = (b << 22) | (b >> 10); + b += c; + + + // ---- Round 2 -------- + + a += (((b ^ c) & d) ^ c) + (uint) K[16] + buff[1]; + a = (a << 5) | (a >> 27); + a += b; + + d += (((a ^ b) & c) ^ b) + (uint) K[17] + buff[6]; + d = (d << 9) | (d >> 23); + d += a; + + c += (((d ^ a) & b) ^ a) + (uint) K[18] + buff[11]; + c = (c << 14) | (c >> 18); + c += d; + + b += (((c ^ d) & a) ^ d) + (uint) K[19] + buff[0]; + b = (b << 20) | (b >> 12); + b += c; + + a += (((b ^ c) & d) ^ c) + (uint) K[20] + buff[5]; + a = (a << 5) | (a >> 27); + a += b; + + d += (((a ^ b) & c) ^ b) + (uint) K[21] + buff[10]; + d = (d << 9) | (d >> 23); + d += a; + + c += (((d ^ a) & b) ^ a) + (uint) K[22] + buff[15]; + c = (c << 14) | (c >> 18); + c += d; + + b += (((c ^ d) & a) ^ d) + (uint) K[23] + buff[4]; + b = (b << 20) | (b >> 12); + b += c; + + a += (((b ^ c) & d) ^ c) + (uint) K[24] + buff[9]; + a = (a << 5) | (a >> 27); + a += b; + + d += (((a ^ b) & c) ^ b) + (uint) K[25] + buff[14]; + d = (d << 9) | (d >> 23); + d += a; + + c += (((d ^ a) & b) ^ a) + (uint) K[26] + buff[3]; + c = (c << 14) | (c >> 18); + c += d; + + b += (((c ^ d) & a) ^ d) + (uint) K[27] + buff[8]; + b = (b << 20) | (b >> 12); + b += c; + + a += (((b ^ c) & d) ^ c) + (uint) K[28] + buff[13]; + a = (a << 5) | (a >> 27); + a += b; + + d += (((a ^ b) & c) ^ b) + (uint) K[29] + buff[2]; + d = (d << 9) | (d >> 23); + d += a; + + c += (((d ^ a) & b) ^ a) + (uint) K[30] + buff[7]; + c = (c << 14) | (c >> 18); + c += d; + + b += (((c ^ d) & a) ^ d) + (uint) K[31] + buff[12]; + b = (b << 20) | (b >> 12); + b += c; + + + // ---- Round 3 -------- + + a += (b ^ c ^ d) + (uint) K[32] + buff[5]; + a = (a << 4) | (a >> 28); + a += b; + + d += (a ^ b ^ c) + (uint) K[33] + buff[8]; + d = (d << 11) | (d >> 21); + d += a; + + c += (d ^ a ^ b) + (uint) K[34] + buff[11]; + c = (c << 16) | (c >> 16); + c += d; + + b += (c ^ d ^ a) + (uint) K[35] + buff[14]; + b = (b << 23) | (b >> 9); + b += c; + + a += (b ^ c ^ d) + (uint) K[36] + buff[1]; + a = (a << 4) | (a >> 28); + a += b; + + d += (a ^ b ^ c) + (uint) K[37] + buff[4]; + d = (d << 11) | (d >> 21); + d += a; + + c += (d ^ a ^ b) + (uint) K[38] + buff[7]; + c = (c << 16) | (c >> 16); + c += d; + + b += (c ^ d ^ a) + (uint) K[39] + buff[10]; + b = (b << 23) | (b >> 9); + b += c; + + a += (b ^ c ^ d) + (uint) K[40] + buff[13]; + a = (a << 4) | (a >> 28); + a += b; + + d += (a ^ b ^ c) + (uint) K[41] + buff[0]; + d = (d << 11) | (d >> 21); + d += a; + + c += (d ^ a ^ b) + (uint) K[42] + buff[3]; + c = (c << 16) | (c >> 16); + c += d; + + b += (c ^ d ^ a) + (uint) K[43] + buff[6]; + b = (b << 23) | (b >> 9); + b += c; + + a += (b ^ c ^ d) + (uint) K[44] + buff[9]; + a = (a << 4) | (a >> 28); + a += b; + + d += (a ^ b ^ c) + (uint) K[45] + buff[12]; + d = (d << 11) | (d >> 21); + d += a; + + c += (d ^ a ^ b) + (uint) K[46] + buff[15]; + c = (c << 16) | (c >> 16); + c += d; + + b += (c ^ d ^ a) + (uint) K[47] + buff[2]; + b = (b << 23) | (b >> 9); + b += c; + + + // ---- Round 4 -------- + + a += (((~d) | b) ^ c) + (uint) K[48] + buff[0]; + a = (a << 6) | (a >> 26); + a += b; + + d += (((~c) | a) ^ b) + (uint) K[49] + buff[7]; + d = (d << 10) | (d >> 22); + d += a; + + c += (((~b) | d) ^ a) + (uint) K[50] + buff[14]; + c = (c << 15) | (c >> 17); + c += d; + + b += (((~a) | c) ^ d) + (uint) K[51] + buff[5]; + b = (b << 21) | (b >> 11); + b += c; + + a += (((~d) | b) ^ c) + (uint) K[52] + buff[12]; + a = (a << 6) | (a >> 26); + a += b; + + d += (((~c) | a) ^ b) + (uint) K[53] + buff[3]; + d = (d << 10) | (d >> 22); + d += a; + + c += (((~b) | d) ^ a) + (uint) K[54] + buff[10]; + c = (c << 15) | (c >> 17); + c += d; + + b += (((~a) | c) ^ d) + (uint) K[55] + buff[1]; + b = (b << 21) | (b >> 11); + b += c; + + a += (((~d) | b) ^ c) + (uint) K[56] + buff[8]; + a = (a << 6) | (a >> 26); + a += b; + + d += (((~c) | a) ^ b) + (uint) K[57] + buff[15]; + d = (d << 10) | (d >> 22); + d += a; + + c += (((~b) | d) ^ a) + (uint) K[58] + buff[6]; + c = (c << 15) | (c >> 17); + c += d; + + b += (((~a) | c) ^ d) + (uint) K[59] + buff[13]; + b = (b << 21) | (b >> 11); + b += c; + + a += (((~d) | b) ^ c) + (uint) K[60] + buff[4]; + a = (a << 6) | (a >> 26); + a += b; + + d += (((~c) | a) ^ b) + (uint) K[61] + buff[11]; + d = (d << 10) | (d >> 22); + d += a; + + c += (((~b) | d) ^ a) + (uint) K[62] + buff[2]; + c = (c << 15) | (c >> 17); + c += d; + + b += (((~a) | c) ^ d) + (uint) K[63] + buff[9]; + b = (b << 21) | (b >> 11); + b += c; + + _H[0] += a; + _H[1] += b; + _H[2] += c; + _H[3] += d; + } + + void ProcessFinalBlock (byte[] inbuf, int startIndex, int length) + { + ulong total = count + (ulong) length; + int padding = (int)(56 - total % BLOCK_SIZE_BYTES); + + if (padding < 1) + padding += BLOCK_SIZE_BYTES; + + var block = new byte [length + padding + 8]; + + for (int i = 0; i < length; i++) + block[i] = inbuf[startIndex + i]; + + block[length] = 0x80; + for (int i = length + 1; i < length + padding; i++) + block[i] = 0x00; + + // I deal in bytes. The algorithm deals in bits. + ulong size = total << 3; + AddLength (size, block, length+padding); + ProcessBlock (block, 0); + + if (length + padding + 8 == 128) + ProcessBlock (block, 64); + } + + void AddLength (ulong length, byte[] buffer, int index) + { + buffer[index++] = (byte) length; + buffer[index++] = (byte) (length >> 8); + buffer[index++] = (byte) (length >> 16); + buffer[index++] = (byte) (length >> 24); + buffer[index++] = (byte) (length >> 32); + buffer[index++] = (byte) (length >> 40); + buffer[index++] = (byte) (length >> 48); + buffer[index] = (byte) (length >> 56); + } + + /// + /// Computes the MD5 hash code for the specified subrange of the buffer. + /// + /// + /// Computes the MD5 hash code for the specified subrange of the buffer. + /// + /// The computed hash code. + /// The buffer. + /// The starting offset. + /// The number of bytes to hash. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// The MD5 context has been disposed. + /// + public byte[] ComputeHash (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || offset > buffer.Length - count) + throw new ArgumentOutOfRangeException ("count"); + + if (disposed) + throw new ObjectDisposedException ("HashAlgorithm"); + + HashCore (buffer, offset, count); + hashValue = HashFinal (); + Initialize (); + + return hashValue; + } + + /// + /// Computes the MD5 hash code for the buffer. + /// + /// + /// Computes the MD5 hash code for the buffer. + /// + /// The computed hash code. + /// The buffer. + /// + /// is null. + /// + /// + /// The MD5 context has been disposed. + /// + public byte[] ComputeHash (byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + return ComputeHash (buffer, 0, buffer.Length); + } + + /// + /// Computes the MD5 hash code for the stream. + /// + /// + /// Computes the MD5 hash code for the stream. + /// + /// The computed hash code. + /// The input stream. + /// + /// is null. + /// + /// + /// The MD5 context has been disposed. + /// + public byte[] ComputeHash (Stream inputStream) + { + if (inputStream == null) + throw new ArgumentNullException ("inputStream"); + + // don't read stream unless object is ready to use + if (disposed) + throw new ObjectDisposedException ("HashAlgorithm"); + + var buffer = new byte [4096]; + int nread; + + do { + if ((nread = inputStream.Read (buffer, 0, buffer.Length)) > 0) + HashCore (buffer, 0, nread); + } while (nread > 0); + + hashValue = HashFinal (); + Initialize (); + + return hashValue; + } + + /// + /// Computes a partial MD5 hash value for the specified region of the + /// input buffer and copies the input into the output buffer. + /// + /// + /// Computes a partial MD5 hash value for the specified region of the + /// input buffer and copies the input into the output buffer. + /// Use to complete the computation + /// of the MD5 hash code. + /// + /// The number of bytes copied into the output buffer. + /// The input buffer. + /// The input buffer offset. + /// The input count. + /// The output buffer. + /// The output buffer offset. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the . + /// -or- + /// is outside the bounds of the + /// . + /// -or- + /// is not large enough to hold the range of input + /// starting at . + /// + public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + if (inputBuffer == null) + throw new ArgumentNullException ("inputBuffer"); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException ("inputOffset"); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException ("inputCount"); + + if (outputBuffer != null) { + if (outputOffset < 0 || outputOffset > outputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException ("outputOffset"); + } + + HashCore (inputBuffer, inputOffset, inputCount); + + if (outputBuffer != null) + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + + return inputCount; + } + + /// + /// Completes the MD5 hash compuation given the final block of input. + /// + /// + /// Completes the MD5 hash compuation given the final block of input. + /// + /// A new buffer containing the specified range of input. + /// The input buffer. + /// The input buffer offset. + /// The input count. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the . + /// + public byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputBuffer == null) + throw new ArgumentNullException ("inputBuffer"); + + if (inputOffset < 0 || inputOffset > inputBuffer.Length) + throw new ArgumentOutOfRangeException ("inputOffset"); + + if (inputCount < 0 || inputOffset > inputBuffer.Length - inputCount) + throw new ArgumentOutOfRangeException ("inputCount"); + + var outputBuffer = new byte [inputCount]; + + // note: other exceptions are handled by Buffer.BlockCopy + Buffer.BlockCopy (inputBuffer, inputOffset, outputBuffer, 0, inputCount); + + HashCore (inputBuffer, inputOffset, inputCount); + hashValue = HashFinal (); + Initialize (); + + return outputBuffer; + } + + void Dispose (bool disposing) + { + if (queuedData != null) { + Array.Clear (queuedData, 0, queuedData.Length); + queuedData = null; + } + + if (_H != null) { + Array.Clear (_H, 0, _H.Length); + _H = null; + } + + if (buff != null) { + Array.Clear (buff, 0, buff.Length); + buff = null; + } + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After calling + /// , you must release all references to the so the + /// garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + disposed = true; + } + } +} diff --git a/src/MimeKit/Cryptography/MacSecureMimeContext.cs b/src/MimeKit/Cryptography/MacSecureMimeContext.cs new file mode 100644 index 0000000..5788738 --- /dev/null +++ b/src/MimeKit/Cryptography/MacSecureMimeContext.cs @@ -0,0 +1,267 @@ +// +// MacSecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2014 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.X509.Store; + +using MimeKit.MacInterop; + +namespace MimeKit.Cryptography { + public class MacSecureMimeContext : SecureMimeContext + { + SecKeychain keychain; + + public MacSecureMimeContext (string path, string password) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (password == null) + throw new ArgumentNullException ("password"); + + keychain = SecKeychain.Create (path, password); + } + + public MacSecureMimeContext () + { + keychain = SecKeychain.Default; + } + + #region implemented abstract members of SecureMimeContext + + /// + /// Gets the X.509 certificate based on the selector. + /// + /// The certificate on success; otherwise null. + /// The search criteria for the certificate. + protected override X509Certificate GetCertificate (IX509Selector selector) + { + foreach (var certificate in keychain.GetCertificates ((CssmKeyUse) 0)) { + if (selector.Match (certificate)) + return certificate; + } + + return null; + } + + /// + /// Gets the private key based on the provided selector. + /// + /// The private key on success; otherwise null. + /// The search criteria for the private key. + protected override AsymmetricKeyParameter GetPrivateKey (IX509Selector selector) + { + foreach (var signer in keychain.GetAllCmsSigners ()) { + if (selector.Match (signer.Certificate)) + return signer.PrivateKey; + } + + return null; + } + + /// + /// Gets the trust anchors. + /// + /// The trust anchors. + protected override Org.BouncyCastle.Utilities.Collections.HashSet GetTrustedAnchors () + { + var anchors = new Org.BouncyCastle.Utilities.Collections.HashSet (); + + // FIXME: how do we get the trusted root certs? + + return anchors; + } + + /// + /// Gets the intermediate certificates. + /// + /// The intermediate certificates. + protected override IX509Store GetIntermediateCertificates () + { + var store = new X509CertificateStore (); + + foreach (var certificate in keychain.GetCertificates ((CssmKeyUse) 0)) { + store.Add (certificate); + } + + return store; + } + + /// + /// Gets the certificate revocation lists. + /// + /// The certificate revocation lists. + protected override IX509Store GetCertificateRevocationLists () + { + var crls = new List (); + + return X509StoreFactory.Create ("Crl/Collection", new X509CollectionStoreParameters (crls)); + } + + /// + /// Gets the for the specified mailbox. + /// + /// A . + /// The mailbox. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox) + { + foreach (var certificate in keychain.GetCertificates (CssmKeyUse.Encrypt)) { + if (certificate.GetSubjectEmailAddress () == mailbox.Address) + return new CmsRecipient (certificate); + } + + throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found."); + } + + /// + /// Gets the for the specified mailbox. + /// + /// A . + /// The mailbox. + /// The preferred digest algorithm. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo) + { + foreach (var signer in keychain.GetAllCmsSigners ()) { + if (signer.Certificate.GetSubjectEmailAddress () == mailbox.Address) { + signer.DigestAlgorithm = digestAlgo; + return signer; + } + } + + throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found."); + } + + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// The certificate. + /// The encryption algorithm capabilities of the client (in preferred order). + /// The timestamp. + protected override void UpdateSecureMimeCapabilities (X509Certificate certificate, EncryptionAlgorithm[] algorithms, DateTime timestamp) + { + // FIXME: implement this + } + + /// + /// Import the specified certificate. + /// + /// The certificate. + /// + /// is null. + /// + public override void Import (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException ("certificate"); + + keychain.Add (certificate); + } + + /// + /// Import the specified certificate revocation list. + /// + /// The certificate revocation list. + /// + /// is null. + /// + public override void Import (X509Crl crl) + { + if (crl == null) + throw new ArgumentNullException ("crl"); + + // FIXME: implement this + } + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// The raw certificate and key data. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// Importing keys is not supported by this cryptography context. + /// + public override void Import (Stream stream, string password) + { + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (password == null) + throw new ArgumentNullException ("password"); + + var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ()); + foreach (string alias in pkcs12.Aliases) { + if (pkcs12.IsKeyEntry (alias)) { + var chain = pkcs12.GetCertificateChain (alias); + var entry = pkcs12.GetKey (alias); + + for (int i = 0; i < chain.Length; i++) + keychain.Add (chain[i].Certificate); + + keychain.Add (entry.Key); + } else if (pkcs12.IsCertificateEntry (alias)) { + var entry = pkcs12.GetCertificate (alias); + keychain.Add (entry.Certificate); + } + } + } + + #endregion + + /// + /// Releases all resources used by the object. + /// + /// If true, this method is being called by + /// ; + /// otherwise it is being called by the finalizer. + protected override void Dispose (bool disposing) + { + if (disposing && keychain != SecKeychain.Default) { + keychain.Dispose (); + keychain = null; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/MultipartEncrypted.cs b/src/MimeKit/Cryptography/MultipartEncrypted.cs new file mode 100644 index 0000000..d6a69fc --- /dev/null +++ b/src/MimeKit/Cryptography/MultipartEncrypted.cs @@ -0,0 +1,1207 @@ +// +// MultipartEncrypted.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Bcpg.OpenPgp; + +using MimeKit.IO; +using MimeKit.IO.Filters; + +namespace MimeKit.Cryptography { + /// + /// A multipart MIME part with a ContentType of multipart/encrypted containing an encrypted MIME part. + /// + /// + /// This mime-type is common when dealing with PGP/MIME but is not used for S/MIME. + /// + public class MultipartEncrypted : Multipart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MultipartEncrypted (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public MultipartEncrypted () : base ("encrypted") + { + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipartEncrypted (this); + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for singing and encrypting. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// -or- + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (OpenPgpContext ctx, MailboxAddress signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.SignAndEncrypt (signer, digestAlgo, cipherAlgo, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for signing and encrypting. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The private key for could not be found. + /// + /// + /// A public key for one or more of the could not be found. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (OpenPgpContext ctx, MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.SignAndEncrypt (signer, digestAlgo, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// A default has not been registered. + /// -or- + /// The is not supported. + /// -or- + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return SignAndEncrypt (ctx, signer, digestAlgo, cipherAlgo, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// A default has not been registered. + /// + /// + /// The private key for could not be found. + /// + /// + /// A public key for one or more of the could not be found. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return SignAndEncrypt (ctx, signer, digestAlgo, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for singing and encrypting. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// -or- + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (OpenPgpContext ctx, PgpSecretKey signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.SignAndEncrypt (signer, digestAlgo, cipherAlgo, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for singing and encrypting. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (OpenPgpContext ctx, PgpSecretKey signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + + entity.WriteTo (options, memory); + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.SignAndEncrypt (signer, digestAlgo, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// A default has not been registered. + /// -or- + /// The is not supported. + /// -or- + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return SignAndEncrypt (ctx, signer, digestAlgo, cipherAlgo, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by signing and encrypting the specified entity. + /// + /// + /// Signs the entity using the supplied signer and digest algorithm and then encrypts to + /// the specified recipients, encapsulating the result in a new multipart/encrypted part. + /// + /// A new instance containing + /// the signed and encrypted version of the specified entity. + /// The signer to use to sign the entity. + /// The digest algorithm to use for signing. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The was out of range. + /// + /// + /// A default has not been registered. + /// -or- + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public static MultipartEncrypted SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, IEnumerable recipients, MimeEntity entity) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return SignAndEncrypt (ctx, signer, digestAlgo, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for encrypting. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + /// + /// THe specified encryption algorithm is not supported. + /// + public static MultipartEncrypted Encrypt (OpenPgpContext ctx, EncryptionAlgorithm algorithm, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (memory)) { + filtered.Add (new Unix2DosFilter ()); + + entity.WriteTo (filtered); + filtered.Flush (); + } + + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.Encrypt (algorithm, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for encrypting. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// A public key for one or more of the could not be found. + /// + public static MultipartEncrypted Encrypt (OpenPgpContext ctx, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (memory)) { + filtered.Add (new Unix2DosFilter ()); + + entity.WriteTo (filtered); + filtered.Flush (); + } + + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.Encrypt (recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + /// + /// A default has not been registered. + /// -or- + /// The specified encryption algorithm is not supported. + /// + public static MultipartEncrypted Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return Encrypt (ctx, algorithm, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A default has not been registered. + /// + /// + /// A public key for one or more of the could not be found. + /// + public static MultipartEncrypted Encrypt (IEnumerable recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return Encrypt (ctx, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for encrypting. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + /// + /// THe specified encryption algorithm is not supported. + /// + public static MultipartEncrypted Encrypt (OpenPgpContext ctx, EncryptionAlgorithm algorithm, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (memory)) { + filtered.Add (new Unix2DosFilter ()); + + entity.WriteTo (filtered); + filtered.Flush (); + } + + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.Encrypt (algorithm, recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The OpenPGP cryptography context to use for encrypting. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + public static MultipartEncrypted Encrypt (OpenPgpContext ctx, IEnumerable recipients, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (memory)) { + filtered.Add (new Unix2DosFilter ()); + + entity.WriteTo (filtered); + filtered.Flush (); + } + + memory.Position = 0; + + var encrypted = new MultipartEncrypted (); + encrypted.ContentType.Parameters["protocol"] = ctx.EncryptionProtocol; + + // add the protocol version part + encrypted.Add (new ApplicationPgpEncrypted ()); + + // add the encrypted entity as the second part + encrypted.Add (ctx.Encrypt (recipients, memory)); + + return encrypted; + } + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The encryption algorithm. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + /// + /// A default has not been registered. + /// -or- + /// The specified encryption algorithm is not supported. + /// + public static MultipartEncrypted Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return Encrypt (ctx, algorithm, recipients, entity); + } + + /// + /// Create a multipart/encrypted MIME part by encrypting the specified entity. + /// + /// + /// Encrypts the entity to the specified recipients, encapsulating the result in a + /// new multipart/encrypted part. + /// + /// A new instance containing + /// the encrypted version of the specified entity. + /// The recipients for the encrypted entity. + /// The entity to sign and encrypt. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// + /// + /// A default has not been registered. + /// + public static MultipartEncrypted Encrypt (IEnumerable recipients, MimeEntity entity) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-encrypted")) + return Encrypt (ctx, recipients, entity); + } + + /// + /// Decrypts the part. + /// + /// + /// Decrypts the and extracts any digital signatures in cases + /// where the content was also signed. + /// + /// The decrypted entity. + /// The OpenPGP cryptography context to use for decrypting. + /// A list of digital signatures if the data was both signed and encrypted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// The provided does not support the protocol parameter. + /// + /// + /// The private key could not be found to decrypt the encrypted data. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimeEntity Decrypt (OpenPgpContext ctx, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + var protocol = ContentType.Parameters["protocol"]?.Trim (); + if (string.IsNullOrEmpty (protocol)) + throw new FormatException (); + + if (!ctx.Supports (protocol)) + throw new NotSupportedException (); + + if (Count < 2) + throw new FormatException (); + + var version = this[0] as MimePart; + if (version == null) + throw new FormatException (); + + var ctype = version.ContentType; + var value = string.Format ("{0}/{1}", ctype.MediaType, ctype.MediaSubtype); + if (!value.Equals (protocol, StringComparison.OrdinalIgnoreCase)) + throw new FormatException (); + + var encrypted = this[1] as MimePart; + if (encrypted == null || encrypted.Content == null) + throw new FormatException (); + + if (!encrypted.ContentType.IsMimeType ("application", "octet-stream")) + throw new FormatException (); + + using (var memory = new MemoryBlockStream ()) { + encrypted.Content.DecodeTo (memory, cancellationToken); + memory.Position = 0; + + return ctx.Decrypt (memory, out signatures, cancellationToken); + } + } + + /// + /// Decrypts the part. + /// + /// + /// Decrypts the part. + /// + /// The decrypted entity. + /// The OpenPGP cryptography context to use for decrypting. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// The provided does not support the protocol parameter. + /// + /// + /// The private key could not be found to decrypt the encrypted data. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimeEntity Decrypt (OpenPgpContext ctx, CancellationToken cancellationToken = default (CancellationToken)) + { + return Decrypt (ctx, out _, cancellationToken); + } + + /// + /// Decrypts the part. + /// + /// + /// Decrypts the and extracts any digital signatures in cases + /// where the content was also signed. + /// + /// The decrypted entity. + /// A list of digital signatures if the data was both signed and encrypted. + /// The cancellation token. + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// A suitable for + /// decrypting could not be found. + /// + /// + /// The private key could not be found to decrypt the encrypted data. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimeEntity Decrypt (out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + var protocol = ContentType.Parameters["protocol"]?.Trim (); + if (string.IsNullOrEmpty (protocol)) + throw new FormatException (); + + if (Count < 2) + throw new FormatException (); + + var version = this[0] as MimePart; + if (version == null) + throw new FormatException (); + + var ctype = version.ContentType; + var value = string.Format ("{0}/{1}", ctype.MediaType, ctype.MediaSubtype); + if (!value.Equals (protocol, StringComparison.OrdinalIgnoreCase)) + throw new FormatException (); + + var encrypted = this[1] as MimePart; + if (encrypted == null || encrypted.Content == null) + throw new FormatException (); + + if (!encrypted.ContentType.IsMimeType ("application", "octet-stream")) + throw new FormatException (); + + using (var ctx = CryptographyContext.Create (protocol)) { + using (var memory = new MemoryBlockStream ()) { + var pgp = ctx as OpenPgpContext; + + encrypted.Content.DecodeTo (memory, cancellationToken); + memory.Position = 0; + + if (pgp != null) + return pgp.Decrypt (memory, out signatures, cancellationToken); + + signatures = null; + + return ctx.Decrypt (memory, cancellationToken); + } + } + } + + /// + /// Decrypts the part. + /// + /// + /// Decrypts the part. + /// + /// The decrypted entity. + /// The cancellation token. + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// A suitable for + /// decrypting could not be found. + /// + /// + /// The private key could not be found to decrypt the encrypted data. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimeEntity Decrypt (CancellationToken cancellationToken = default (CancellationToken)) + { + return Decrypt (out _, cancellationToken); + } + } +} diff --git a/src/MimeKit/Cryptography/MultipartSigned.cs b/src/MimeKit/Cryptography/MultipartSigned.cs new file mode 100644 index 0000000..04b30c0 --- /dev/null +++ b/src/MimeKit/Cryptography/MultipartSigned.cs @@ -0,0 +1,581 @@ +// +// MultipartSigned.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Org.BouncyCastle.Bcpg.OpenPgp; + +using MimeKit.IO; +using MimeKit.IO.Filters; + +namespace MimeKit.Cryptography { + /// + /// A signed multipart, as used by both S/MIME and PGP/MIME protocols. + /// + /// + /// The first child of a multipart/signed is the content while the second child + /// is the detached signature data. Any other children are not defined and could + /// be anything. + /// + public class MultipartSigned : Multipart + { + /// + /// Initialize a new instance of the class. + /// + /// This constructor is used by . + /// Information used by the constructor. + /// + /// is null. + /// + public MultipartSigned (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public MultipartSigned () : base ("signed") + { + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipartSigned (this); + } + + static MimeEntity Prepare (MimeEntity entity, Stream memory) + { + entity.Prepare (EncodingConstraint.SevenBit, 78); + + using (var filtered = new FilteredStream (memory)) { + // Note: see rfc3156, section 3 - second note + filtered.Add (new ArmoredFromFilter ()); + + // Note: see rfc3156, section 5.4 (this is the main difference between rfc2015 and rfc3156) + filtered.Add (new TrailingWhitespaceFilter ()); + + // Note: see rfc2015 or rfc3156, section 5.1 + filtered.Add (new Unix2DosFilter ()); + + entity.WriteTo (filtered); + filtered.Flush (); + } + + memory.Position = 0; + + // Note: we need to parse the modified entity structure to preserve any modifications + var parser = new MimeParser (memory, MimeFormat.Entity); + + return parser.ParseEntity (); + } + + static MultipartSigned Create (CryptographyContext ctx, DigestAlgorithm digestAlgo, MimeEntity entity, MimeEntity signature) + { + var micalg = ctx.GetDigestAlgorithmName (digestAlgo); + var signed = new MultipartSigned (); + + // set the protocol and micalg Content-Type parameters + signed.ContentType.Parameters["protocol"] = ctx.SignatureProtocol; + signed.ContentType.Parameters["micalg"] = micalg; + + // add the modified/parsed entity as our first part + signed.Add (entity); + + // add the detached signature as the second part + signed.Add (signature); + + return signed; + } + + /// + /// Creates a new . + /// + /// + /// Cryptographically signs the entity using the supplied signer and digest algorithm in + /// order to generate a detached signature and then adds the entity along with the + /// detached signature data to a new multipart/signed part. + /// + /// A new instance. + /// The cryptography context to use for signing. + /// The signer. + /// The digest algorithm to use for signing. + /// The entity to sign. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// The private key could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static MultipartSigned Create (CryptographyContext ctx, MailboxAddress signer, DigestAlgorithm digestAlgo, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var prepared = Prepare (entity, memory); + + memory.Position = 0; + + // sign the cleartext content + var signature = ctx.Sign (signer, digestAlgo, memory); + + return Create (ctx, digestAlgo, prepared, signature); + } + } + + /// + /// Creates a new . + /// + /// + /// Cryptographically signs the entity using the supplied signer and digest algorithm in + /// order to generate a detached signature and then adds the entity along with the + /// detached signature data to a new multipart/signed part. + /// + /// A new instance. + /// The OpenPGP context to use for signing. + /// The signer. + /// The digest algorithm to use for signing. + /// The entity to sign. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// An error occurred in the OpenPGP subsystem. + /// + public static MultipartSigned Create (OpenPgpContext ctx, PgpSecretKey signer, DigestAlgorithm digestAlgo, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var prepared = Prepare (entity, memory); + + memory.Position = 0; + + // sign the cleartext content + var signature = ctx.Sign (signer, digestAlgo, memory); + + return Create (ctx, digestAlgo, prepared, signature); + } + } + + /// + /// Creates a new . + /// + /// + /// Cryptographically signs the entity using the supplied signer and digest algorithm in + /// order to generate a detached signature and then adds the entity along with the + /// detached signature data to a new multipart/signed part. + /// + /// A new instance. + /// The signer. + /// The digest algorithm to use for signing. + /// The entity to sign. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// + /// + /// The was out of range. + /// + /// + /// A cryptography context suitable for signing could not be found. + /// -or- + /// The is not supported. + /// + /// + /// An error occurred in the OpenPGP subsystem. + /// + public static MultipartSigned Create (PgpSecretKey signer, DigestAlgorithm digestAlgo, MimeEntity entity) + { + using (var ctx = (OpenPgpContext) CryptographyContext.Create ("application/pgp-signature")) + return Create (ctx, signer, digestAlgo, entity); + } + + /// + /// Creates a new . + /// + /// + /// Cryptographically signs the entity using the supplied signer in order + /// to generate a detached signature and then adds the entity along with + /// the detached signature data to a new multipart/signed part. + /// + /// A new instance. + /// The S/MIME context to use for signing. + /// The signer. + /// The entity to sign. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static MultipartSigned Create (SecureMimeContext ctx, CmsSigner signer, MimeEntity entity) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (entity == null) + throw new ArgumentNullException (nameof (entity)); + + using (var memory = new MemoryBlockStream ()) { + var prepared = Prepare (entity, memory); + + memory.Position = 0; + + // sign the cleartext content + var signature = ctx.Sign (signer, memory); + + return Create (ctx, signer.DigestAlgorithm, prepared, signature); + } + } + + /// + /// Creates a new . + /// + /// + /// Cryptographically signs the entity using the supplied signer in order + /// to generate a detached signature and then adds the entity along with + /// the detached signature data to a new multipart/signed part. + /// + /// A new instance. + /// The signer. + /// The entity to sign. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A cryptography context suitable for signing could not be found. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public static MultipartSigned Create (CmsSigner signer, MimeEntity entity) + { + using (var ctx = (SecureMimeContext) CryptographyContext.Create ("application/pkcs7-signature")) + return Create (ctx, signer, entity); + } + + /// + /// Prepare the MIME entity for transport using the specified encoding constraints. + /// + /// + /// Prepares the MIME entity for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public override void Prepare (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + // Note: we do not iterate over our children because they are already signed + // and changing them would break the signature. They should already be + // properly prepared, anyway. + } + + /// + /// Verify the multipart/signed part. + /// + /// + /// Verifies the multipart/signed part using the supplied cryptography context. + /// + /// A signer info collection. + /// The cryptography context to use for verifying the signature. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The multipart is malformed in some way. + /// + /// + /// does not support verifying the signature part. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public DigitalSignatureCollection Verify (CryptographyContext ctx, CancellationToken cancellationToken = default (CancellationToken)) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + var protocol = ContentType.Parameters["protocol"]?.Trim (); + if (string.IsNullOrEmpty (protocol)) + throw new FormatException ("The multipart/signed part did not specify a protocol."); + + if (!ctx.Supports (protocol)) + throw new NotSupportedException ("The specified cryptography context does not support the signature protocol."); + + if (Count < 2) + throw new FormatException ("The multipart/signed part did not contain the expected children."); + + var signature = this[1] as MimePart; + if (signature == null || signature.Content == null) + throw new FormatException ("The signature part could not be found."); + + var ctype = signature.ContentType; + var value = string.Format ("{0}/{1}", ctype.MediaType, ctype.MediaSubtype); + if (!ctx.Supports (value)) + throw new NotSupportedException (string.Format ("The specified cryptography context does not support '{0}'.", value)); + + using (var signatureData = new MemoryBlockStream ()) { + signature.Content.DecodeTo (signatureData, cancellationToken); + signatureData.Position = 0; + + using (var cleartext = new MemoryBlockStream ()) { + // Note: see rfc2015 or rfc3156, section 5.1 + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + options.VerifyingSignature = true; + + this[0].WriteTo (options, cleartext); + cleartext.Position = 0; + + return ctx.Verify (cleartext, signatureData, cancellationToken); + } + } + } + + /// + /// Verify the multipart/signed part. + /// + /// + /// Verifies the multipart/signed part using the supplied cryptography context. + /// + /// A signer info collection. + /// The cryptography context to use for verifying the signature. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The multipart is malformed in some way. + /// + /// + /// does not support verifying the signature part. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public async Task VerifyAsync (CryptographyContext ctx, CancellationToken cancellationToken = default (CancellationToken)) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + var protocol = ContentType.Parameters["protocol"]?.Trim (); + if (string.IsNullOrEmpty (protocol)) + throw new FormatException ("The multipart/signed part did not specify a protocol."); + + if (!ctx.Supports (protocol)) + throw new NotSupportedException ("The specified cryptography context does not support the signature protocol."); + + if (Count < 2) + throw new FormatException ("The multipart/signed part did not contain the expected children."); + + var signature = this[1] as MimePart; + if (signature == null || signature.Content == null) + throw new FormatException ("The signature part could not be found."); + + var ctype = signature.ContentType; + var value = string.Format ("{0}/{1}", ctype.MediaType, ctype.MediaSubtype); + if (!ctx.Supports (value)) + throw new NotSupportedException (string.Format ("The specified cryptography context does not support '{0}'.", value)); + + using (var signatureData = new MemoryBlockStream ()) { + await signature.Content.DecodeToAsync (signatureData, cancellationToken).ConfigureAwait (false); + signatureData.Position = 0; + + using (var cleartext = new MemoryBlockStream ()) { + // Note: see rfc2015 or rfc3156, section 5.1 + var options = FormatOptions.CloneDefault (); + options.NewLineFormat = NewLineFormat.Dos; + options.VerifyingSignature = true; + + await this[0].WriteToAsync (options, cleartext, cancellationToken); + cleartext.Position = 0; + + return await ctx.VerifyAsync (cleartext, signatureData, cancellationToken).ConfigureAwait (false); + } + } + } + + /// + /// Verify the multipart/signed part. + /// + /// + /// Verifies the multipart/signed part using the default cryptography context. + /// + /// A signer info collection. + /// The cancellation token. + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// A cryptography context suitable for verifying the signature could not be found. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public DigitalSignatureCollection Verify (CancellationToken cancellationToken = default (CancellationToken)) + { + var protocol = ContentType.Parameters["protocol"]?.Trim (); + + if (string.IsNullOrEmpty (protocol)) + throw new FormatException ("The multipart/signed part did not specify a protocol."); + + using (var ctx = CryptographyContext.Create (protocol)) + return Verify (ctx, cancellationToken); + } + + /// + /// Asynchronously verify the multipart/signed part. + /// + /// + /// Verifies the multipart/signed part using the default cryptography context. + /// + /// A signer info collection. + /// The cancellation token. + /// + /// The protocol parameter was not specified. + /// -or- + /// The multipart is malformed in some way. + /// + /// + /// A cryptography context suitable for verifying the signature could not be found. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public Task VerifyAsync (CancellationToken cancellationToken = default (CancellationToken)) + { + var protocol = ContentType.Parameters["protocol"]?.Trim (); + + if (string.IsNullOrEmpty (protocol)) + throw new FormatException ("The multipart/signed part did not specify a protocol."); + + using (var ctx = CryptographyContext.Create (protocol)) + return VerifyAsync (ctx, cancellationToken); + } + } +} diff --git a/src/MimeKit/Cryptography/NpgsqlCertificateDatabase.cs b/src/MimeKit/Cryptography/NpgsqlCertificateDatabase.cs new file mode 100644 index 0000000..1bee953 --- /dev/null +++ b/src/MimeKit/Cryptography/NpgsqlCertificateDatabase.cs @@ -0,0 +1,264 @@ +// +// NpgsqlCertificateDatabase.cs +// +// Author: Federico Di Gregorio +// +// Copyright (c) 2013-2019 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Data; +using System.Text; +using System.Reflection; +using System.Data.Common; +using System.Collections.Generic; + +namespace MimeKit.Cryptography { + /// + /// An X.509 certificate database built on PostgreSQL. + /// + /// + /// An X.509 certificate database is used for storing certificates, metdata related to the certificates + /// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs), + /// and private keys. + /// This particular database uses PostgreSQL to store the data. + /// + public class NpgsqlCertificateDatabase : SqlCertificateDatabase + { + static readonly Type npgsqlConnectionClass; + static readonly Assembly npgsqlAssembly; + + static NpgsqlCertificateDatabase () + { + try { + npgsqlAssembly = Assembly.Load ("Npgsql"); + if (npgsqlAssembly != null) { + npgsqlConnectionClass = npgsqlAssembly.GetType ("Npgsql.NpgsqlConnection"); + + IsAvailable = true; + } + } catch (FileNotFoundException) { + } catch (FileLoadException) { + } catch (BadImageFormatException) { + } + } + + internal static bool IsAvailable { + get; private set; + } + + static DbConnection CreateConnection (string connectionString) + { + if (connectionString == null) + throw new ArgumentNullException (nameof (connectionString)); + + if (connectionString.Length == 0) + throw new ArgumentException ("The connection string cannot be empty.", nameof (connectionString)); + + return (DbConnection) Activator.CreateInstance (npgsqlConnectionClass, new [] { connectionString }); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new and opens a connection to the + /// PostgreSQL database using the specified connection string. + /// + /// The connection string. + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// + public NpgsqlCertificateDatabase (string connectionString, string password) : base (CreateConnection (connectionString), password) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Creates a new using the provided Npgsql database connection. + /// + /// The Npgsql connection. + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + public NpgsqlCertificateDatabase (DbConnection connection, string password) : base (connection, password) + { + } + + /// + /// Gets the columns for the specified table. + /// + /// + /// Gets the list of columns for the specified table. + /// + /// The . + /// The name of the table. + /// The list of columns. + protected override IList GetTableColumns (DbConnection connection, string tableName) + { + using (var command = connection.CreateCommand ()) { + command.CommandText = $"PRAGMA table_info({tableName})"; + using (var reader = command.ExecuteReader ()) { + var columns = new List (); + + while (reader.Read ()) { + var column = new DataColumn (); + + for (int i = 0; i < reader.FieldCount; i++) { + var field = reader.GetName (i).ToUpperInvariant (); + + switch (field) { + case "NAME": + column.ColumnName = reader.GetString (i); + break; + case "TYPE": + var type = reader.GetString (i); + switch (type) { + case "boolean": column.DataType = typeof (bool); break; + case "integer": column.DataType = typeof (long); break; + case "bytea": column.DataType = typeof (byte[]); break; + case "text": column.DataType = typeof (string); break; + } + break; + case "NOTNULL": + column.AllowDBNull = !reader.GetBoolean (i); + break; + } + } + + columns.Add (column); + } + + return columns; + } + } + } + + static void Build (StringBuilder statement, DataTable table, DataColumn column, ref int primaryKeys) + { + statement.Append (column.ColumnName); + statement.Append (' '); + + if (column.DataType == typeof (long) || column.DataType == typeof (int)) { + if (column.AutoIncrement) + statement.Append ("serial"); + else + statement.Append ("integer"); + } else if (column.DataType == typeof (bool)) { + statement.Append ("boolean"); + } else if (column.DataType == typeof (byte[])) { + statement.Append ("bytea"); + } else if (column.DataType == typeof (string)) { + statement.Append ("text"); + } else { + throw new NotImplementedException (); + } + + bool isPrimaryKey = false; + if (table != null && table.PrimaryKey != null && primaryKeys < table.PrimaryKey.Length) { + for (int i = 0; i < table.PrimaryKey.Length; i++) { + if (column == table.PrimaryKey[i]) { + statement.Append (" PRIMARY KEY"); + isPrimaryKey = true; + primaryKeys++; + break; + } + } + } + + if (column.Unique && !isPrimaryKey) + statement.Append (" UNIQUE"); + + if (!column.AllowDBNull) + statement.Append (" NOT NULL"); + } + + /// + /// Create a table. + /// + /// + /// Creates the specified table. + /// + /// The . + /// The table. + protected override void CreateTable (DbConnection connection, DataTable table) + { + var statement = new StringBuilder ("CREATE TABLE IF NOT EXISTS "); + int primaryKeys = 0; + + statement.Append (table.TableName); + statement.Append ('('); + + foreach (DataColumn column in table.Columns) { + Build (statement, table, column, ref primaryKeys); + statement.Append (", "); + } + + if (table.Columns.Count > 0) + statement.Length -= 2; + + statement.Append (')'); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + + /// + /// Adds a column to a table. + /// + /// + /// Adds a column to a table. + /// + /// The . + /// The table. + /// The column to add. + protected override void AddTableColumn (DbConnection connection, DataTable table, DataColumn column) + { + var statement = new StringBuilder ("ALTER TABLE "); + int primaryKeys = table.PrimaryKey?.Length ?? 0; + + statement.Append (table.TableName); + statement.Append (" ADD COLUMN "); + Build (statement, table, column, ref primaryKeys); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpBlockFilter.cs b/src/MimeKit/Cryptography/OpenPgpBlockFilter.cs new file mode 100644 index 0000000..d5c109b --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpBlockFilter.cs @@ -0,0 +1,208 @@ +// +// OpenPgpArmoredFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.Utils; +using MimeKit.IO.Filters; + +namespace MimeKit.Cryptography { + /// + /// A filter to strip off data before and after an armored OpenPGP block. + /// + /// + /// Filters out data before and after armored OpenPGP blocks. + /// + class OpenPgpBlockFilter : MimeFilterBase + { + readonly byte[] beginMarker; + readonly byte[] endMarker; + bool seenBeginMarker; + bool seenEndMarker; + bool midline; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// An OpenPGP begin marker. + /// An OpenPGP end marker. + public OpenPgpBlockFilter (string beginMarker, string endMarker) + { + this.beginMarker = CharsetUtils.UTF8.GetBytes (beginMarker); + this.endMarker = CharsetUtils.UTF8.GetBytes (endMarker); + } + + static bool IsMarker (byte[] input, int startIndex, byte[] marker) + { + int i = startIndex; + + for (int j = 0; j < marker.Length; i++, j++) { + if (input[i] != marker[j]) + return false; + } + + if (input[i] == (byte) '\r') + i++; + + return input[i] == (byte) '\n'; + } + + static bool IsPartialMatch (byte[] input, int startIndex, int endIndex, byte[] marker) + { + int i = startIndex; + + for (int j = 0; j < marker.Length && i < endIndex; i++, j++) { + if (input[i] != marker[j]) + return false; + } + + if (i < endIndex && input[i] == (byte) '\r') + i++; + + return i == endIndex; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int endIndex = startIndex + length; + int index = startIndex; + + outputIndex = startIndex; + outputLength = 0; + + if (seenEndMarker || length == 0) + return input; + + if (midline) { + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + if (seenBeginMarker) + outputLength = index - startIndex; + + return input; + } + + midline = false; + } + + if (!seenBeginMarker) { + do { + int lineIndex = index; + + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + if (IsPartialMatch (input, lineIndex, index, beginMarker)) + SaveRemainingInput (input, lineIndex, index - lineIndex); + else + midline = true; + return input; + } + + index++; + + if (IsMarker (input, lineIndex, beginMarker)) { + outputLength = index - lineIndex; + outputIndex = lineIndex; + seenBeginMarker = true; + break; + } + } while (index < endIndex); + + if (index == endIndex) + return input; + } + + do { + int lineIndex = index; + + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + if (!flush) { + if (IsPartialMatch (input, lineIndex, index, endMarker)) { + SaveRemainingInput (input, lineIndex, index - lineIndex); + outputLength = lineIndex - outputIndex; + } else { + outputLength = index - outputIndex; + midline = true; + } + + return input; + } + + outputLength = index - outputIndex; + return input; + } + + index++; + + if (IsMarker (input, lineIndex, endMarker)) { + seenEndMarker = true; + break; + } + } while (index < endIndex); + + outputLength = index - outputIndex; + + return input; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + seenBeginMarker = false; + seenEndMarker = false; + midline = false; + + base.Reset (); + } + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpContext.cs b/src/MimeKit/Cryptography/OpenPgpContext.cs new file mode 100644 index 0000000..37c504d --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpContext.cs @@ -0,0 +1,1185 @@ +// +// OpenPgpContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Bcpg; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto.Prng; +using Org.BouncyCastle.Bcpg.OpenPgp; +using Org.BouncyCastle.Crypto.Parameters; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// An abstract OpenPGP cryptography context which can be used for OpenPGP and PGP/MIME that + /// manages keyrings stored on the local file system as keyring bundles. + /// + /// + /// PGP software such as older versions of GnuPG (pre 2.1.0) typically store the user's + /// keyrings on the file system using the OpenPGP Keyring Bundle format. + /// Generally speaking, applications should not use a + /// directly, but rather via higher level APIs such as + /// and . + /// + public abstract class OpenPgpContext : OpenPgpContextBase + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Subclasses choosing to use this constructor MUST set the , + /// , , and the + /// properties themselves. + /// + protected OpenPgpContext () : base () + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new using the specified public and private keyring paths. + /// + /// The public keyring file path. + /// The secret keyring file path. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred while reading one of the keyring files. + /// + /// + /// An error occurred while parsing one of the keyring files. + /// + protected OpenPgpContext (string pubring, string secring) : this () + { + if (pubring == null) + throw new ArgumentNullException (nameof (pubring)); + + if (secring == null) + throw new ArgumentNullException (nameof (secring)); + + PublicKeyRingPath = pubring; + SecretKeyRingPath = secring; + + if (File.Exists (pubring)) { + using (var file = File.OpenRead (pubring)) { + PublicKeyRingBundle = new PgpPublicKeyRingBundle (file); + } + } else { + PublicKeyRingBundle = new PgpPublicKeyRingBundle (new byte[0]); + } + + if (File.Exists (secring)) { + using (var file = File.OpenRead (secring)) { + SecretKeyRingBundle = new PgpSecretKeyRingBundle (file); + } + } else { + SecretKeyRingBundle = new PgpSecretKeyRingBundle (new byte[0]); + } + } + + /// + /// Get the public keyring path. + /// + /// + /// Gets the public keyring path. + /// + /// The public key ring path. + protected string PublicKeyRingPath { + get; set; + } + + /// + /// Get the secret keyring path. + /// + /// + /// Gets the secret keyring path. + /// + /// The secret key ring path. + protected string SecretKeyRingPath { + get; set; + } + + /// + /// Get the public keyring bundle. + /// + /// + /// Gets the public keyring bundle. + /// + /// The public keyring bundle. + public PgpPublicKeyRingBundle PublicKeyRingBundle { + get; protected set; + } + + /// + /// Get the secret keyring bundle. + /// + /// + /// Gets the secret keyring bundle. + /// + /// The secret keyring bundle. + public PgpSecretKeyRingBundle SecretKeyRingBundle { + get; protected set; + } + + bool TryGetPublicKeyRing (long keyId, out PgpPublicKeyRing keyring) + { + foreach (PgpPublicKeyRing ring in PublicKeyRingBundle.GetKeyRings ()) { + foreach (PgpPublicKey key in ring.GetPublicKeys ()) { + if (key.KeyId == keyId) { + keyring = ring; + return true; + } + } + } + + keyring = null; + + return false; + } + + /// + /// Get the public keyring that contains the specified key. + /// + /// + /// Gets the public keyring that contains the specified key. + /// Implementations should first try to obtain the keyring stored (or cached) locally. + /// Failing that, if is enabled, they should use + /// to attempt to + /// retrieve the keyring from the configured . + /// + /// The public key identifier. + /// The cancellation token. + /// The public keyring that contains the specified key or null if the keyring could not be found. + /// + /// The operation was cancelled. + /// + protected override PgpPublicKeyRing GetPublicKeyRing (long keyId, CancellationToken cancellationToken) + { + if (TryGetPublicKeyRing (keyId, out var keyring)) + return keyring; + + if (AutoKeyRetrieve) + return RetrievePublicKeyRing (keyId, cancellationToken); + + return null; + } + + /// + /// Asynchronously get the public keyring that contains the specified key. + /// + /// + /// Gets the public keyring that contains the specified key. + /// Implementations should first try to obtain the keyring stored (or cached) locally. + /// Failing that, if is enabled, they should use + /// to attempt to + /// retrieve the keyring from the configured . + /// + /// The public key identifier. + /// The cancellation token. + /// The public keyring that contains the specified key or null if the keyring could not be found. + /// + /// The operation was cancelled. + /// + protected override async Task GetPublicKeyRingAsync (long keyId, CancellationToken cancellationToken) + { + if (TryGetPublicKeyRing (keyId, out var keyring)) + return keyring; + + if (AutoKeyRetrieve) + return await RetrievePublicKeyRingAsync (keyId, cancellationToken).ConfigureAwait (false); + + return null; + } + + /// + /// Enumerate all public keyrings. + /// + /// + /// Enumerates all public keyrings. + /// + /// The list of available public keyrings. + public virtual IEnumerable EnumeratePublicKeyRings () + { + foreach (PgpPublicKeyRing keyring in PublicKeyRingBundle.GetKeyRings ()) + yield return keyring; + + yield break; + } + + /// + /// Enumerate all public keys. + /// + /// + /// Enumerates all public keys. + /// + /// The list of available public keys. + public virtual IEnumerable EnumeratePublicKeys () + { + foreach (var keyring in EnumeratePublicKeyRings ()) { + foreach (PgpPublicKey key in keyring.GetPublicKeys ()) + yield return key; + } + + yield break; + } + + /// + /// Enumerate the public keyrings for a particular mailbox. + /// + /// + /// Enumerates all public keyrings for the specified mailbox. + /// + /// The public keys. + /// Mailbox. + /// + /// is null. + /// + public virtual IEnumerable EnumeratePublicKeyRings (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var keyring in EnumeratePublicKeyRings ()) { + if (IsMatch (keyring.GetPublicKey (), mailbox)) + yield return keyring; + } + + yield break; + } + + /// + /// Enumerate the public keys for a particular mailbox. + /// + /// + /// Enumerates all public keys for the specified mailbox. + /// + /// The public keys. + /// The mailbox address. + /// + /// is null. + /// + public virtual IEnumerable EnumeratePublicKeys (MailboxAddress mailbox) + { + foreach (var keyring in EnumeratePublicKeyRings (mailbox)) { + foreach (PgpPublicKey key in keyring.GetPublicKeys ()) + yield return key; + } + + yield break; + } + + /// + /// Enumerate all secret keyrings. + /// + /// + /// Enumerates all secret keyrings. + /// + /// The list of available secret keyrings. + public virtual IEnumerable EnumerateSecretKeyRings () + { + foreach (PgpSecretKeyRing keyring in SecretKeyRingBundle.GetKeyRings ()) + yield return keyring; + + yield break; + } + + /// + /// Enumerate all secret keys. + /// + /// + /// Enumerates all secret keys. + /// + /// The list of available secret keys. + public virtual IEnumerable EnumerateSecretKeys () + { + foreach (var keyring in EnumerateSecretKeyRings ()) { + foreach (PgpSecretKey key in keyring.GetSecretKeys ()) + yield return key; + } + + yield break; + } + + /// + /// Enumerate the secret keyrings for a particular mailbox. + /// + /// + /// Enumerates all secret keyrings for the specified mailbox. + /// + /// The secret keys. + /// The mailbox address. + /// + /// is null. + /// + public virtual IEnumerable EnumerateSecretKeyRings (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var keyring in EnumerateSecretKeyRings ()) { + if (IsMatch (keyring.GetSecretKey (), mailbox)) + yield return keyring; + } + + yield break; + } + + /// + /// Enumerate the secret keys for a particular mailbox. + /// + /// + /// Enumerates all secret keys for the specified mailbox. + /// + /// The public keys. + /// The mailbox address. + /// + /// is null. + /// + public virtual IEnumerable EnumerateSecretKeys (MailboxAddress mailbox) + { + foreach (var keyring in EnumerateSecretKeyRings (mailbox)) { + foreach (PgpSecretKey key in keyring.GetSecretKeys ()) + yield return key; + } + + yield break; + } + + /// + /// Get the public key associated with the mailbox address. + /// + /// + /// Gets a valid public key associated with the mailbox address that can be used for encryption. + /// + /// The public encryption key. + /// The mailbox. + /// + /// is null. + /// + /// + /// The public key for the specified could not be found. + /// + protected virtual PgpPublicKey GetPublicKey (MailboxAddress mailbox) + { + foreach (var key in EnumeratePublicKeys (mailbox)) { + if (!key.IsEncryptionKey || key.IsRevoked () || IsExpired (key)) + continue; + + return key; + } + + throw new PublicKeyNotFoundException (mailbox, "The public key could not be found."); + } + + /// + /// Get the public keys for the specified mailbox addresses. + /// + /// + /// Gets a list of valid public keys for the specified mailbox addresses that can be used for encryption. + /// + /// The encryption keys. + /// The mailboxes. + /// + /// is null. + /// + /// + /// A public key for one or more of the could not be found. + /// + public override IList GetPublicKeys (IEnumerable mailboxes) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + var keys = new List (); + + foreach (var mailbox in mailboxes) + keys.Add (GetPublicKey (mailbox)); + + return keys; + } + + /// + /// Get the secret key for a specified key identifier. + /// + /// + /// Gets the secret key for a specified key identifier. + /// + /// The key identifier for the desired secret key. + /// The secret key. + /// + /// The secret key specified by the could not be found. + /// + protected override PgpSecretKey GetSecretKey (long keyId) + { + foreach (var key in EnumerateSecretKeys ()) { + if (key.KeyId == keyId) + return key; + } + + throw new PrivateKeyNotFoundException (keyId, "The secret key could not be found."); + } + + /// + /// Get the signing key associated with the mailbox address. + /// + /// + /// Gets the signing key associated with the mailbox address. + /// + /// The signing key. + /// The mailbox. + /// + /// is null. + /// + /// + /// A secret key for the specified could not be found. + /// + public override PgpSecretKey GetSigningKey (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var keyring in EnumerateSecretKeyRings (mailbox)) { + foreach (PgpSecretKey key in keyring.GetSecretKeys ()) { + if (!key.IsSigningKey) + continue; + + var pubkey = key.PublicKey; + if (pubkey.IsRevoked () || IsExpired (pubkey)) + continue; + + return key; + } + } + + throw new PrivateKeyNotFoundException (mailbox, "The private key could not be found."); + } + + /// + /// Check whether or not a particular mailbox address can be used for signing. + /// + /// + /// Checks whether or not as particular mailbocx address can be used for signing. + /// + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. + /// + /// is null. + /// + public override bool CanSign (MailboxAddress signer) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + foreach (var key in EnumerateSecretKeys (signer)) { + if (!key.IsSigningKey) + continue; + + var pubkey = key.PublicKey; + if (pubkey.IsRevoked () || IsExpired (pubkey)) + continue; + + return true; + } + + return false; + } + + /// + /// Check whether or not the cryptography context can encrypt to a particular recipient. + /// + /// + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. + /// + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. + /// + public override bool CanEncrypt (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var key in EnumeratePublicKeys (mailbox)) { + if (!key.IsEncryptionKey || key.IsRevoked () || IsExpired (key)) + continue; + + return true; + } + + return false; + } + +#if false + /// + /// Gets the private key. + /// + /// + /// Gets the private key. + /// + /// The private key. + /// The key identifier. + /// + /// The specified secret key could not be found. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + protected PgpPrivateKey GetPrivateKey (long keyId) + { + var secret = GetSecretKey (keyId); + + return GetPrivateKey (secret); + } + + PublicKeyAlgorithmTag GetPublicKeyAlgorithmTag (PublicKeyAlgorithm algorithm) + { + switch (algorithm) { + case PublicKeyAlgorithm.DiffieHellman: return PublicKeyAlgorithmTag.DiffieHellman; + case PublicKeyAlgorithm.Dsa: return PublicKeyAlgorithmTag.Dsa; + case PublicKeyAlgorithm.EdwardsCurveDsa: throw new NotSupportedException ("EDDSA is not currently supported."); + case PublicKeyAlgorithm.ElGamalEncrypt: return PublicKeyAlgorithmTag.ElGamalEncrypt; + case PublicKeyAlgorithm.ElGamalGeneral: return PublicKeyAlgorithmTag.ElGamalGeneral; + case PublicKeyAlgorithm.EllipticCurve: return PublicKeyAlgorithmTag.ECDH; + case PublicKeyAlgorithm.EllipticCurveDsa: return PublicKeyAlgorithmTag.ECDsa; + case PublicKeyAlgorithm.RsaEncrypt: return PublicKeyAlgorithmTag.RsaEncrypt; + case PublicKeyAlgorithm.RsaGeneral: return PublicKeyAlgorithmTag.RsaGeneral; + case PublicKeyAlgorithm.RsaSign: return PublicKeyAlgorithmTag.RsaSign; + default: throw new ArgumentOutOfRangeException (nameof (algorithm)); + } + } +#endif + + void AddEncryptionKeyPair (PgpKeyRingGenerator keyRingGenerator, KeyGenerationParameters parameters, PublicKeyAlgorithmTag algorithm, DateTime now, long expirationTime, int[] encryptionAlgorithms, int[] digestAlgorithms) + { + var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator ("RSA"); + + keyPairGenerator.Init (parameters); + + var keyPair = new PgpKeyPair (algorithm, keyPairGenerator.GenerateKeyPair (), now); + var subpacketGenerator = new PgpSignatureSubpacketGenerator (); + + subpacketGenerator.SetKeyFlags (false, PgpKeyFlags.CanEncryptCommunications | PgpKeyFlags.CanEncryptStorage); + subpacketGenerator.SetPreferredSymmetricAlgorithms (false, encryptionAlgorithms); + subpacketGenerator.SetPreferredHashAlgorithms (false, digestAlgorithms); + + if (expirationTime > 0) { + subpacketGenerator.SetKeyExpirationTime (false, expirationTime); + subpacketGenerator.SetSignatureExpirationTime (false, expirationTime); + } + + keyRingGenerator.AddSubKey (keyPair, subpacketGenerator.Generate (), null); + } + + PgpKeyRingGenerator CreateKeyRingGenerator (MailboxAddress mailbox, EncryptionAlgorithm algorithm, long expirationTime, string password, DateTime now, SecureRandom random) + { + var enabledEncryptionAlgorithms = EnabledEncryptionAlgorithms; + var enabledDigestAlgorithms = EnabledDigestAlgorithms; + var encryptionAlgorithms = new int[enabledEncryptionAlgorithms.Length]; + var digestAlgorithms = new int[enabledDigestAlgorithms.Length]; + + for (int i = 0; i < enabledEncryptionAlgorithms.Length; i++) + encryptionAlgorithms[i] = (int) enabledEncryptionAlgorithms[i]; + for (int i = 0; i < enabledDigestAlgorithms.Length; i++) + digestAlgorithms[i] = (int) enabledDigestAlgorithms[i]; + + var parameters = new RsaKeyGenerationParameters (BigInteger.ValueOf (0x10001), random, 2048, 12); + var signingAlgorithm = PublicKeyAlgorithmTag.RsaSign; + + var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator ("RSA"); + + keyPairGenerator.Init (parameters); + + var signingKeyPair = new PgpKeyPair (signingAlgorithm, keyPairGenerator.GenerateKeyPair (), now); + + var subpacketGenerator = new PgpSignatureSubpacketGenerator (); + subpacketGenerator.SetKeyFlags (false, PgpKeyFlags.CanSign | PgpKeyFlags.CanCertify); + subpacketGenerator.SetPreferredSymmetricAlgorithms (false, encryptionAlgorithms); + subpacketGenerator.SetPreferredHashAlgorithms (false, digestAlgorithms); + + if (expirationTime > 0) { + subpacketGenerator.SetKeyExpirationTime (false, expirationTime); + subpacketGenerator.SetSignatureExpirationTime (false, expirationTime); + } + + subpacketGenerator.SetFeature (false, Org.BouncyCastle.Bcpg.Sig.Features.FEATURE_MODIFICATION_DETECTION); + + var keyRingGenerator = new PgpKeyRingGenerator ( + PgpSignature.PositiveCertification, + signingKeyPair, + mailbox.ToString (false), + GetSymmetricKeyAlgorithm (algorithm), + CharsetUtils.UTF8.GetBytes (password), + true, + subpacketGenerator.Generate (), + null, + random); + + // Add the (optional) encryption subkey. + AddEncryptionKeyPair (keyRingGenerator, parameters, PublicKeyAlgorithmTag.RsaGeneral, now, expirationTime, encryptionAlgorithms, digestAlgorithms); + + return keyRingGenerator; + } + + /// + /// Generate a new key pair. + /// + /// + /// Generates a new RSA key pair. + /// + /// The mailbox to generate the key pair for. + /// The password to be set on the secret key. + /// The expiration date for the generated key pair. + /// The symmetric key algorithm to use. + /// The source of randomness to use when generating the key pair. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a date in the future. + /// + public void GenerateKeyPair (MailboxAddress mailbox, string password, DateTime? expirationDate = null, EncryptionAlgorithm algorithm = EncryptionAlgorithm.Aes256, SecureRandom random = null) + { + var now = DateTime.UtcNow; + long expirationTime = 0; + + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + if (expirationDate.HasValue) { + var utc = expirationDate.Value.ToUniversalTime (); + + if (utc <= now) + throw new ArgumentException ("expirationDate needs to be greater than DateTime.Now", nameof (expirationDate)); + + if ((expirationTime = Convert.ToInt64 (utc.Subtract (now).TotalSeconds)) <= 0) + throw new ArgumentException ("expirationDate needs to be greater than DateTime.Now", nameof (expirationDate)); + } + + if (random == null) { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + random = new SecureRandom (new CryptoApiRandomGenerator ()); +#else + random = new SecureRandom (); +#endif + } + + var generator = CreateKeyRingGenerator (mailbox, algorithm, expirationTime, password, now, random); + + Import (generator.GenerateSecretKeyRing ()); + Import (generator.GeneratePublicKeyRing ()); + } + + /// + /// Sign a public key. + /// + /// + /// Signs a public key using the specified secret key. + /// Most OpenPGP implementations use + /// to make their "key signatures". Some implementations are known to use the other + /// certification types, but few differentiate between them. + /// + /// The secret key to use for signing. + /// The public key to sign. + /// The digest algorithm. + /// The certification to give the signed key. + /// + /// is null. + /// -or- + /// is null. + /// + public void SignKey (PgpSecretKey secretKey, PgpPublicKey publicKey, DigestAlgorithm digestAlgo = DigestAlgorithm.Sha1, OpenPgpKeyCertification certification = OpenPgpKeyCertification.GenericCertification) + { + if (secretKey == null) + throw new ArgumentNullException (nameof (secretKey)); + + if (publicKey == null) + throw new ArgumentNullException (nameof (publicKey)); + + var privateKey = GetPrivateKey (secretKey); + var signatureGenerator = new PgpSignatureGenerator (secretKey.PublicKey.Algorithm, GetHashAlgorithm (digestAlgo)); + + signatureGenerator.InitSign ((int) certification, privateKey); + signatureGenerator.GenerateOnePassVersion (false); + + var subpacketGenerator = new PgpSignatureSubpacketGenerator (); + var subpacketVector = subpacketGenerator.Generate (); + + signatureGenerator.SetHashedSubpackets (subpacketVector); + + var signedKey = PgpPublicKey.AddCertification (publicKey, signatureGenerator.Generate ()); + PgpPublicKeyRing keyring = null; + + foreach (var ring in EnumeratePublicKeyRings ()) { + foreach (PgpPublicKey key in ring.GetPublicKeys ()) { + if (key.KeyId == publicKey.KeyId) { + PublicKeyRingBundle = PgpPublicKeyRingBundle.RemovePublicKeyRing (PublicKeyRingBundle, ring); + keyring = PgpPublicKeyRing.InsertPublicKey (ring, signedKey); + break; + } + } + } + + if (keyring == null) + keyring = new PgpPublicKeyRing (signedKey.GetEncoded ()); + + Import (keyring); + } + + /// + /// Saves the public key-ring bundle. + /// + /// + /// Atomically saves the public key-ring bundle to the path specified by . + /// Called by if any public keys were successfully imported. + /// + /// + /// An error occured while saving the public key-ring bundle. + /// + protected void SavePublicKeyRingBundle () + { + var filename = Path.GetFileName (PublicKeyRingPath) + "~"; + var dirname = Path.GetDirectoryName (PublicKeyRingPath); + var tmp = Path.Combine (dirname, "." + filename); + var bak = Path.Combine (dirname, filename); + + Directory.CreateDirectory (dirname); + + using (var file = File.Open (tmp, FileMode.Create, FileAccess.Write)) { + PublicKeyRingBundle.Encode (file); + file.Flush (); + } + + if (File.Exists (PublicKeyRingPath)) { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + File.Replace (tmp, PublicKeyRingPath, bak); +#else + if (File.Exists (bak)) + File.Delete (bak); + File.Move (PublicKeyRingPath, bak); + File.Move (tmp, PublicKeyRingPath); +#endif + } else { + File.Move (tmp, PublicKeyRingPath); + } + } + + /// + /// Saves the secret key-ring bundle. + /// + /// + /// Atomically saves the secret key-ring bundle to the path specified by . + /// Called by if any secret keys were successfully imported. + /// + /// + /// An error occured while saving the secret key-ring bundle. + /// + protected void SaveSecretKeyRingBundle () + { + var filename = Path.GetFileName (SecretKeyRingPath) + "~"; + var dirname = Path.GetDirectoryName (SecretKeyRingPath); + var tmp = Path.Combine (dirname, "." + filename); + var bak = Path.Combine (dirname, filename); + + Directory.CreateDirectory (dirname); + + using (var file = File.Open (tmp, FileMode.Create, FileAccess.Write)) { + SecretKeyRingBundle.Encode (file); + file.Flush (); + } + + if (File.Exists (SecretKeyRingPath)) { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + File.Replace (tmp, SecretKeyRingPath, bak); +#else + if (File.Exists (bak)) + File.Delete (bak); + File.Move (SecretKeyRingPath, bak); + File.Move (tmp, SecretKeyRingPath); +#endif + } else { + File.Move (tmp, SecretKeyRingPath); + } + } + + /// + /// Imports a public pgp keyring. + /// + /// + /// Imports a public pgp keyring. + /// + /// The pgp keyring. + /// + /// is null. + /// + public virtual void Import (PgpPublicKeyRing keyring) + { + if (keyring == null) + throw new ArgumentNullException (nameof (keyring)); + + PublicKeyRingBundle = PgpPublicKeyRingBundle.AddPublicKeyRing (PublicKeyRingBundle, keyring); + SavePublicKeyRingBundle (); + } + + /// + /// Imports a public pgp keyring bundle. + /// + /// + /// Imports a public pgp keyring bundle. + /// + /// The pgp keyring bundle. + /// + /// is null. + /// + public override void Import (PgpPublicKeyRingBundle bundle) + { + if (bundle == null) + throw new ArgumentNullException (nameof (bundle)); + + int publicKeysAdded = 0; + + foreach (PgpPublicKeyRing pubring in bundle.GetKeyRings ()) { + PublicKeyRingBundle = PgpPublicKeyRingBundle.AddPublicKeyRing (PublicKeyRingBundle, pubring); + publicKeysAdded++; + } + + if (publicKeysAdded > 0) + SavePublicKeyRingBundle (); + } + + /// + /// Imports public pgp keys from the specified stream. + /// + /// + /// Imports public pgp keys from the specified stream. + /// + /// The raw key data. + /// + /// is null. + /// + /// + /// An error occurred while parsing the raw key-ring data + /// -or- + /// An error occured while saving the public key-ring bundle. + /// + public override void Import (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + using (var armored = new ArmoredInputStream (stream)) + Import (new PgpPublicKeyRingBundle (armored)); + } + + /// + /// Imports a secret pgp keyring. + /// + /// + /// Imports a secret pgp keyring. + /// + /// The pgp keyring. + /// + /// is null. + /// + public virtual void Import (PgpSecretKeyRing keyring) + { + if (keyring == null) + throw new ArgumentNullException (nameof (keyring)); + + SecretKeyRingBundle = PgpSecretKeyRingBundle.AddSecretKeyRing (SecretKeyRingBundle, keyring); + SaveSecretKeyRingBundle (); + } + + /// + /// Imports a secret pgp keyring bundle. + /// + /// + /// Imports a secret pgp keyring bundle. + /// + /// The pgp keyring bundle. + /// + /// is null. + /// + public virtual void Import (PgpSecretKeyRingBundle bundle) + { + if (bundle == null) + throw new ArgumentNullException (nameof (bundle)); + + int secretKeysAdded = 0; + + foreach (PgpSecretKeyRing secring in bundle.GetKeyRings ()) { + SecretKeyRingBundle = PgpSecretKeyRingBundle.AddSecretKeyRing (SecretKeyRingBundle, secring); + secretKeysAdded++; + } + + if (secretKeysAdded > 0) + SaveSecretKeyRingBundle (); + } + + /// + /// Exports the public keys for the specified mailboxes. + /// + /// + /// Exports the public keys for the specified mailboxes. + /// + /// A new instance containing the exported public keys. + /// The mailboxes associated with the public keys to export. + /// + /// is null. + /// + /// + /// was empty. + /// + public override MimePart Export (IEnumerable mailboxes) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + var keyrings = new List (); + foreach (var mailbox in mailboxes) + keyrings.AddRange (EnumeratePublicKeyRings (mailbox)); + + var bundle = new PgpPublicKeyRingBundle (keyrings); + + return Export (bundle); + } + + /// + /// Exports the specified public keys. + /// + /// + /// Exports the specified public keys. + /// + /// A new instance containing the exported public keys. + /// The public keys to export. + /// + /// is null. + /// + public MimePart Export (IEnumerable keys) + { + if (keys == null) + throw new ArgumentNullException (nameof (keys)); + + var keyrings = keys.Select (key => new PgpPublicKeyRing (key.GetEncoded ())); + var bundle = new PgpPublicKeyRingBundle (keyrings); + + return Export (bundle); + } + + /// + /// Export the specified public keys. + /// + /// + /// Exports the specified public keys. + /// + /// A new instance containing the exported public keys. + /// The public keys to export. + /// + /// is null. + /// + public MimePart Export (PgpPublicKeyRingBundle keys) + { + if (keys == null) + throw new ArgumentNullException (nameof (keys)); + + var content = new MemoryBlockStream (); + + Export (keys, content, true); + + content.Position = 0; + + return new MimePart ("application", "pgp-keys") { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), + Content = new MimeContent (content) + }; + } + + /// + /// Export the public keyrings for the specified mailboxes. + /// + /// + /// Exports the public keyrings for the specified mailboxes. + /// + /// The mailboxes. + /// The output stream. + /// true if the output should be armored; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An I/O error occurred. + /// + public void Export (IEnumerable mailboxes, Stream stream, bool armor) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var keyrings = new List (); + foreach (var mailbox in mailboxes) + keyrings.AddRange (EnumeratePublicKeyRings (mailbox)); + + var bundle = new PgpPublicKeyRingBundle (keyrings); + + Export (bundle, stream, armor); + } + + /// + /// Export the specified public keys. + /// + /// + /// Exports the specified public keys. + /// + /// A new instance containing the exported public keys. + /// The public keys to export. + /// The output stream. + /// true if the output should be armored; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An I/O error occurred. + /// + public void Export (IEnumerable keys, Stream stream, bool armor) + { + if (keys == null) + throw new ArgumentNullException (nameof (keys)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var keyrings = keys.Select (key => new PgpPublicKeyRing (key.GetEncoded ())); + var bundle = new PgpPublicKeyRingBundle (keyrings); + + Export (bundle, stream, armor); + } + + /// + /// Export the public keyring bundle. + /// + /// + /// Exports the public keyring bundle. + /// + /// The public keyring bundle to export. + /// The output stream. + /// true if the output should be armored; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An I/O error occurred. + /// + public void Export (PgpPublicKeyRingBundle keys, Stream stream, bool armor) + { + if (keys == null) + throw new ArgumentNullException (nameof (keys)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (armor) { + using (var armored = new ArmoredOutputStream (stream)) { + armored.SetHeader ("Version", null); + + keys.Encode (armored); + armored.Flush (); + } + } else { + keys.Encode (stream); + } + } + + /// + /// Delete a public pgp keyring. + /// + /// + /// Deletes a public pgp keyring. + /// + /// The pgp keyring. + /// + /// is null. + /// + public virtual void Delete (PgpPublicKeyRing keyring) + { + if (keyring == null) + throw new ArgumentNullException (nameof (keyring)); + + PublicKeyRingBundle = PgpPublicKeyRingBundle.RemovePublicKeyRing (PublicKeyRingBundle, keyring); + SavePublicKeyRingBundle (); + } + + /// + /// Delete a secret pgp keyring. + /// + /// + /// Deletes a secret pgp keyring. + /// + /// The pgp keyring. + /// + /// is null. + /// + public virtual void Delete (PgpSecretKeyRing keyring) + { + if (keyring == null) + throw new ArgumentNullException (nameof (keyring)); + + SecretKeyRingBundle = PgpSecretKeyRingBundle.RemoveSecretKeyRing (SecretKeyRingBundle, keyring); + SaveSecretKeyRingBundle (); + } + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpContextBase.cs b/src/MimeKit/Cryptography/OpenPgpContextBase.cs new file mode 100644 index 0000000..5a9b8a8 --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpContextBase.cs @@ -0,0 +1,1887 @@ +// +// OpenPgpContext.cs +// +// Authors: Jeffrey Stedfast +// Thomas Hansen +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Net.Http; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Bcpg; +using Org.BouncyCastle.Bcpg.OpenPgp; + +using MimeKit.IO; + +namespace MimeKit.Cryptography { + /// + /// An abstract OpenPGP cryptography context which can be used for PGP/MIME. + /// + /// + /// Generally speaking, applications should not use a + /// directly, but rather via higher level APIs such as + /// and . + /// + public abstract class OpenPgpContextBase : CryptographyContext + { + static readonly string[] ProtocolSubtypes = { "pgp-signature", "pgp-encrypted", "pgp-keys", "x-pgp-signature", "x-pgp-encrypted", "x-pgp-keys" }; + const string BeginPublicKeyBlock = "-----BEGIN PGP PUBLIC KEY BLOCK-----"; + const string EndPublicKeyBlock = "-----END PGP PUBLIC KEY BLOCK-----"; + + static readonly EncryptionAlgorithm[] DefaultEncryptionAlgorithmRank = { + EncryptionAlgorithm.Idea, + EncryptionAlgorithm.TripleDes, + EncryptionAlgorithm.Cast5, + EncryptionAlgorithm.Blowfish, + EncryptionAlgorithm.Aes128, + EncryptionAlgorithm.Aes192, + EncryptionAlgorithm.Aes256, + EncryptionAlgorithm.Twofish, + EncryptionAlgorithm.Camellia128, + EncryptionAlgorithm.Camellia192, + EncryptionAlgorithm.Camellia256 + }; + + static readonly DigestAlgorithm[] DefaultDigestAlgorithmRank = { + DigestAlgorithm.Sha1, + DigestAlgorithm.RipeMD160, + DigestAlgorithm.Sha256, + DigestAlgorithm.Sha384, + DigestAlgorithm.Sha512, + DigestAlgorithm.Sha224 + }; + + EncryptionAlgorithm defaultAlgorithm; + HttpClient client; + Uri keyServer; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + protected OpenPgpContextBase () + { + EncryptionAlgorithmRank = DefaultEncryptionAlgorithmRank; + DigestAlgorithmRank = DefaultDigestAlgorithmRank; + + foreach (var algorithm in EncryptionAlgorithmRank) + Enable (algorithm); + + foreach (var algorithm in DigestAlgorithmRank) + Enable (algorithm); + + defaultAlgorithm = EncryptionAlgorithm.Cast5; + + client = new HttpClient (); + } + + /// + /// Get the password for a secret key. + /// + /// + /// Gets the password for a secret key. + /// + /// The password for the secret key. + /// The secret key. + /// + /// The user chose to cancel the password request. + /// + protected abstract string GetPasswordForKey (PgpSecretKey key); // FIXME: rename this to GetPassword() in the future + + /// + /// Get the public keyring that contains the specified key. + /// + /// + /// Gets the public keyring that contains the specified key. + /// Implementations should first try to obtain the keyring stored (or cached) locally. + /// Failing that, if is enabled, they should use + /// to attempt to + /// retrieve the keyring from the configured . + /// + /// The public key identifier. + /// The cancellation token. + /// The public keyring that contains the specified key or null if the keyring could not be found. + /// + /// The operation was cancelled. + /// + protected abstract PgpPublicKeyRing GetPublicKeyRing (long keyId, CancellationToken cancellationToken); + + /// + /// Get the public keyring that contains the specified key asynchronously. + /// + /// + /// Gets the public keyring that contains the specified key. + /// Implementations should first try to obtain the keyring stored (or cached) locally. + /// Failing that, if is enabled, they should use + /// to attempt to + /// retrieve the keyring from the configured . + /// + /// The public key identifier. + /// The cancellation token. + /// The public keyring that contains the specified key or null if the keyring could not be found. + /// + /// The operation was cancelled. + /// + protected abstract Task GetPublicKeyRingAsync (long keyId, CancellationToken cancellationToken); + + /// + /// Get the secret key for a specified key identifier. + /// + /// + /// Gets the secret key for a specified key identifier. + /// + /// The key identifier for the desired secret key. + /// The secret key. + /// + /// The secret key specified by the could not be found. + /// + protected abstract PgpSecretKey GetSecretKey (long keyId); + + /// + /// Get the public keys for the specified mailbox addresses. + /// + /// + /// Gets a list of valid public keys for the specified mailbox addresses that can be used for encryption. + /// + /// The encryption keys. + /// The mailboxes. + /// + /// is null. + /// + /// + /// A public key for one or more of the could not be found. + /// + public abstract IList GetPublicKeys (IEnumerable mailboxes); + + /// + /// Get the signing key associated with the mailbox address. + /// + /// + /// Gets the signing key associated with the mailbox address. + /// + /// The signing key. + /// The mailbox. + /// + /// is null. + /// + /// + /// A secret key for the specified could not be found. + /// + public abstract PgpSecretKey GetSigningKey (MailboxAddress mailbox); + + /// + /// Get or set the default encryption algorithm. + /// + /// + /// Gets or sets the default encryption algorithm. + /// + /// The encryption algorithm. + /// + /// The specified encryption algorithm is not supported. + /// + public EncryptionAlgorithm DefaultEncryptionAlgorithm { + get { return defaultAlgorithm; } + set { + GetSymmetricKeyAlgorithm (value); + defaultAlgorithm = value; + } + } + + bool IsValidKeyServer { + get { + if (keyServer == null) + return false; + + switch (keyServer.Scheme.ToLowerInvariant ()) { + case "https": case "http": case "hkp": return true; + default: return false; + } + } + } + + /// + /// Get or set the key server to use when automatically retrieving keys. + /// + /// + /// Gets or sets the key server to use when verifying keys that are + /// not already in the public keychain. + /// Only HTTP and HKP protocols are supported. + /// + /// The key server. + /// + /// is not an absolute URI. + /// + public Uri KeyServer { + get { return keyServer; } + set { + if (value != null && !value.IsAbsoluteUri) + throw new ArgumentException ("The key server URI must be absolute.", nameof (value)); + + keyServer = value; + } + } + + /// + /// Get or set whether unknown PGP keys should automtically be retrieved. + /// + /// + /// Gets or sets whether or not the should automatically + /// fetch keys as needed from the keyserver when verifying signatures. + /// Requires a valid to be set. + /// + /// true if unknown PGP keys should automatically be retrieved; otherwise, false. + public bool AutoKeyRetrieve { + get; set; + } + + /// + /// Get the signature protocol. + /// + /// + /// The signature protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The signature protocol. + public override string SignatureProtocol { + get { return "application/pgp-signature"; } + } + + /// + /// Get the encryption protocol. + /// + /// + /// The encryption protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The encryption protocol. + public override string EncryptionProtocol { + get { return "application/pgp-encrypted"; } + } + + /// + /// Get the key exchange protocol. + /// + /// + /// Gets the key exchange protocol. + /// + /// The key exchange protocol. + public override string KeyExchangeProtocol { + get { return "application/pgp-keys"; } + } + + /// + /// Check whether or not the specified protocol is supported. + /// + /// + /// Used in order to make sure that the protocol parameter value specified in either a multipart/signed + /// or multipart/encrypted part is supported by the supplied cryptography context. + /// + /// true if the protocol is supported; otherwise false + /// The protocol. + /// + /// is null. + /// + public override bool Supports (string protocol) + { + if (protocol == null) + throw new ArgumentNullException (nameof (protocol)); + + if (!protocol.StartsWith ("application/", StringComparison.OrdinalIgnoreCase)) + return false; + + int startIndex = "application/".Length; + int subtypeLength = protocol.Length - startIndex; + + for (int i = 0; i < ProtocolSubtypes.Length; i++) { + if (subtypeLength != ProtocolSubtypes[i].Length) + continue; + + if (string.Compare (protocol, startIndex, ProtocolSubtypes[i], 0, subtypeLength, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + + return false; + } + + /// + /// Get the string name of the digest algorithm for use with the micalg parameter of a multipart/signed part. + /// + /// + /// Maps the to the appropriate string identifier + /// as used by the micalg parameter value of a multipart/signed Content-Type + /// header. For example: + /// + /// AlgorithmName + /// pgp-md5 + /// pgp-sha1 + /// pgp-ripemd160 + /// pgp-md2 + /// pgp-tiger192 + /// pgp-haval-5-160 + /// pgp-sha256 + /// pgp-sha384 + /// pgp-sha512 + /// pgp-sha224 + /// + /// + /// The micalg value. + /// The digest algorithm. + /// + /// is out of range. + /// + public override string GetDigestAlgorithmName (DigestAlgorithm micalg) + { + switch (micalg) { + case DigestAlgorithm.MD5: return "pgp-md5"; + case DigestAlgorithm.Sha1: return "pgp-sha1"; + case DigestAlgorithm.RipeMD160: return "pgp-ripemd160"; + case DigestAlgorithm.MD2: return "pgp-md2"; + case DigestAlgorithm.Tiger192: return "pgp-tiger192"; + case DigestAlgorithm.Haval5160: return "pgp-haval-5-160"; + case DigestAlgorithm.Sha256: return "pgp-sha256"; + case DigestAlgorithm.Sha384: return "pgp-sha384"; + case DigestAlgorithm.Sha512: return "pgp-sha512"; + case DigestAlgorithm.Sha224: return "pgp-sha224"; + case DigestAlgorithm.MD4: return "pgp-md4"; + default: throw new ArgumentOutOfRangeException (nameof (micalg)); + } + } + + /// + /// Get the digest algorithm from the micalg parameter value in a multipart/signed part. + /// + /// + /// Maps the micalg parameter value string back to the appropriate . + /// + /// The digest algorithm. + /// The micalg parameter value. + /// + /// is null. + /// + public override DigestAlgorithm GetDigestAlgorithm (string micalg) + { + if (micalg == null) + throw new ArgumentNullException (nameof (micalg)); + + switch (micalg.ToLowerInvariant ()) { + case "pgp-md5": return DigestAlgorithm.MD5; + case "pgp-sha1": return DigestAlgorithm.Sha1; + case "pgp-ripemd160": return DigestAlgorithm.RipeMD160; + case "pgp-md2": return DigestAlgorithm.MD2; + case "pgp-tiger192": return DigestAlgorithm.Tiger192; + case "pgp-haval-5-160": return DigestAlgorithm.Haval5160; + case "pgp-sha256": return DigestAlgorithm.Sha256; + case "pgp-sha384": return DigestAlgorithm.Sha384; + case "pgp-sha512": return DigestAlgorithm.Sha512; + case "pgp-sha224": return DigestAlgorithm.Sha224; + case "pgp-md4": return DigestAlgorithm.MD4; + default: return DigestAlgorithm.None; + } + } + + /// + /// Hex encode an array of bytes. + /// + /// + /// This method is used to hex-encode the PGP key fingerprints. + /// + /// The data to encode. + /// A string representing the hex-encoded data. + static string HexEncode (byte[] data) + { + var fingerprint = new StringBuilder (); + + for (int i = 0; i < data.Length; i++) + fingerprint.Append (data[i].ToString ("x2")); + + return fingerprint.ToString (); + } + + /// + /// Check that a public key is a match for the specified mailbox. + /// + /// + /// Checks that the public key is a match for the specified mailbox. + /// If the is a with a non-empty + /// , then the fingerprint is used to match the key's + /// fingerprint. Otherwise, the email address(es) contained within the key's user identifier strings + /// are compared to the mailbox address. + /// + /// The public key. + /// The mailbox address. + /// true if the key is a match for the specified mailbox; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + protected static bool IsMatch (PgpPublicKey key, MailboxAddress mailbox) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + if (mailbox is SecureMailboxAddress secure && !string.IsNullOrEmpty (secure.Fingerprint)) { + if (secure.Fingerprint.Length > 16) { + var fingerprint = HexEncode (key.GetFingerprint ()); + + return secure.Fingerprint.Equals (fingerprint, StringComparison.OrdinalIgnoreCase); + } + + var id = ((int) key.KeyId).ToString ("X2"); + + return secure.Fingerprint.EndsWith (id, StringComparison.OrdinalIgnoreCase); + } + + foreach (string userId in key.GetUserIds ()) { + if (!MailboxAddress.TryParse (userId, out var email)) + continue; + + if (mailbox.Address.Equals (email.Address, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + /// + /// Check that a secret key is a match for the specified mailbox. + /// + /// + /// Checks that the secret key is a match for the specified mailbox. + /// If the is a with a non-empty + /// , then the fingerprint is used to match the key's + /// fingerprint. Otherwise, the email address(es) contained within the key's user identifier strings + /// are compared to the mailbox address. + /// + /// The secret key. + /// The mailbox address. + /// true if the key is a match for the specified mailbox; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + protected static bool IsMatch (PgpSecretKey key, MailboxAddress mailbox) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + if (mailbox is SecureMailboxAddress secure && !string.IsNullOrEmpty (secure.Fingerprint)) { + if (secure.Fingerprint.Length > 16) { + var fingerprint = HexEncode (key.PublicKey.GetFingerprint ()); + + return secure.Fingerprint.Equals (fingerprint, StringComparison.OrdinalIgnoreCase); + } + + var id = ((int) key.KeyId).ToString ("X2"); + + return secure.Fingerprint.EndsWith (id, StringComparison.OrdinalIgnoreCase); + } + + foreach (string userId in key.UserIds) { + if (!MailboxAddress.TryParse (userId, out var email)) + continue; + + if (mailbox.Address.Equals (email.Address, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + /// + /// Check if a public key is expired. + /// + /// + /// Checks if a public key is expired. + /// + /// The public key. + /// true if the public key is expired; otherwise, false. + /// + /// is null. + /// + protected static bool IsExpired (PgpPublicKey key) + { + if (key == null) + throw new ArgumentNullException (nameof (key)); + + long seconds = key.GetValidSeconds (); + + if (seconds != 0) { + var expires = key.CreationTime.AddSeconds ((double) seconds); + if (expires <= DateTime.Now) + return true; + } + + return false; + } + + /// + /// Retrieves the public keyring, using the preferred key server, automatically importing it afterwards. + /// + /// The identifier of the key to be retrieved. + /// true if this operation should be done asynchronously; otherweise, false. + /// The cancellation token. + /// The public key ring. + async Task RetrievePublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken) + { + if (!IsValidKeyServer) + return null; + + var scheme = keyServer.Scheme.ToLowerInvariant (); + var uri = new UriBuilder (); + + uri.Scheme = scheme == "hkp" ? "http" : scheme; + uri.Host = keyServer.Host; + + if (keyServer.IsDefaultPort) { + if (scheme == "hkp") + uri.Port = 11371; + } else { + uri.Port = keyServer.Port; + } + + uri.Path = "/pks/lookup"; + uri.Query = string.Format ("op=get&search=0x{0:X}", keyId); + + using (var stream = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new OpenPgpBlockFilter (BeginPublicKeyBlock, EndPublicKeyBlock)); + + if (doAsync) { + using (var response = await client.GetAsync (uri.ToString (), cancellationToken).ConfigureAwait (false)) + await response.Content.CopyToAsync (filtered).ConfigureAwait (false); + } else { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var request = (HttpWebRequest) WebRequest.Create (uri.ToString ()); + using (var response = request.GetResponse ()) { + var content = response.GetResponseStream (); + content.CopyTo (filtered, 4096); + } +#else + using (var response = client.GetAsync (uri.ToString (), cancellationToken).GetAwaiter ().GetResult ()) + response.Content.CopyToAsync (filtered).GetAwaiter ().GetResult (); +#endif + } + + filtered.Flush (); + } + + stream.Position = 0; + + using (var armored = new ArmoredInputStream (stream, true)) { + var bundle = new PgpPublicKeyRingBundle (armored); + + Import (bundle); + + return bundle.GetPublicKeyRing (keyId); + } + } + } + + /// + /// Retrieve the public keyring using the configured key server. + /// + /// + /// Retrieves the public keyring specified by the from the key server + /// set on the property. If the keyring is successfully retrieved, it will + /// be imported via . + /// This method should be called by + /// when the keyring is not available locally. + /// + /// The identifier of the public key to be retrieved. + /// The cancellation token. + /// The public key ring. + protected PgpPublicKeyRing RetrievePublicKeyRing (long keyId, CancellationToken cancellationToken) + { + return RetrievePublicKeyRingAsync (keyId, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously retrieve the public keyring using the configured key server. + /// + /// + /// Retrieves the public keyring specified by the from the key server + /// set on the property. If the keyring is successfully retrieved, it will + /// be imported via . + /// This method should be called by + /// when the keyring is not available locally. + /// + /// The identifier of the public key to be retrieved. + /// The cancellation token. + /// The public key ring. + protected Task RetrievePublicKeyRingAsync (long keyId, CancellationToken cancellationToken) + { + return RetrievePublicKeyRingAsync (keyId, true, cancellationToken); + } + + /// + /// Gets the private key from the specified secret key. + /// + /// + /// Gets the private key from the specified secret key. + /// + /// The private key. + /// The secret key. + /// + /// is null. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + protected PgpPrivateKey GetPrivateKey (PgpSecretKey key) + { + int attempts = 0; + string password; + + if (key == null) + throw new ArgumentNullException (nameof (key)); + + do { + if ((password = GetPasswordForKey (key)) == null) + throw new OperationCanceledException (); + + try { + var privateKey = key.ExtractPrivateKey (password.ToCharArray ()); + + // Note: the private key will be null if the private key is empty. + if (privateKey == null) + break; + + return privateKey; + } catch (Exception ex) { +#if DEBUG + Debug.WriteLine (string.Format ("Failed to extract secret key: {0}", ex)); +#endif + } + + attempts++; + } while (attempts < 3); + + throw new UnauthorizedAccessException (); + } + + /// + /// Gets the equivalent for the + /// specified . + /// + /// + /// Maps a to the equivalent . + /// + /// The hash algorithm. + /// The digest algorithm. + /// + /// is out of range. + /// + /// + /// is not a supported digest algorithm. + /// + public static HashAlgorithmTag GetHashAlgorithm (DigestAlgorithm digestAlgo) + { + switch (digestAlgo) { + case DigestAlgorithm.MD5: return HashAlgorithmTag.MD5; + case DigestAlgorithm.Sha1: return HashAlgorithmTag.Sha1; + case DigestAlgorithm.RipeMD160: return HashAlgorithmTag.RipeMD160; + case DigestAlgorithm.DoubleSha: throw new NotSupportedException ("The Double SHA digest algorithm is not supported."); + case DigestAlgorithm.MD2: return HashAlgorithmTag.MD2; + case DigestAlgorithm.Tiger192: throw new NotSupportedException ("The Tiger-192 digest algorithm is not supported."); + case DigestAlgorithm.Haval5160: throw new NotSupportedException ("The HAVAL 5 160 digest algorithm is not supported."); + case DigestAlgorithm.Sha256: return HashAlgorithmTag.Sha256; + case DigestAlgorithm.Sha384: return HashAlgorithmTag.Sha384; + case DigestAlgorithm.Sha512: return HashAlgorithmTag.Sha512; + case DigestAlgorithm.Sha224: return HashAlgorithmTag.Sha224; + case DigestAlgorithm.MD4: throw new NotSupportedException ("The MD4 digest algorithm is not supported."); + default: throw new ArgumentOutOfRangeException (nameof (digestAlgo)); + } + } + + /// + /// Cryptographically signs the content. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing key could not be found for . + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public override MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return Sign (key, digestAlgo, content); + } + + /// + /// Cryptographically signs the content. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public ApplicationPgpSignature Sign (PgpSecretKey signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (!signer.IsSigningKey) + throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var hashAlgorithm = GetHashAlgorithm (digestAlgo); + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + armored.SetHeader ("Version", null); + + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + using (var compressed = compresser.Open (armored)) { + var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); + var buf = new byte[4096]; + int nread; + + signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) + signatureGenerator.Update (buf, 0, nread); + + var signature = signatureGenerator.Generate (); + + signature.Encode (compressed); + compressed.Flush (); + } + + armored.Flush (); + } + + memory.Position = 0; + + return new ApplicationPgpSignature (memory); + } + + /// + /// Gets the equivalent for the specified + /// . + /// + /// + /// Gets the equivalent for the specified + /// . + /// + /// The digest algorithm. + /// The hash algorithm. + /// + /// is out of range. + /// + /// + /// does not have an equivalent value. + /// + public static DigestAlgorithm GetDigestAlgorithm (HashAlgorithmTag hashAlgorithm) + { + switch (hashAlgorithm) { + case HashAlgorithmTag.MD5: return DigestAlgorithm.MD5; + case HashAlgorithmTag.Sha1: return DigestAlgorithm.Sha1; + case HashAlgorithmTag.RipeMD160: return DigestAlgorithm.RipeMD160; + case HashAlgorithmTag.DoubleSha: return DigestAlgorithm.DoubleSha; + case HashAlgorithmTag.MD2: return DigestAlgorithm.MD2; + case HashAlgorithmTag.Tiger192: return DigestAlgorithm.Tiger192; + case HashAlgorithmTag.Haval5pass160: return DigestAlgorithm.Haval5160; + case HashAlgorithmTag.Sha256: return DigestAlgorithm.Sha256; + case HashAlgorithmTag.Sha384: return DigestAlgorithm.Sha384; + case HashAlgorithmTag.Sha512: return DigestAlgorithm.Sha512; + case HashAlgorithmTag.Sha224: return DigestAlgorithm.Sha224; + default: throw new ArgumentOutOfRangeException (nameof (hashAlgorithm)); + } + } + + /// + /// Gets the equivalent for the specified + /// . + /// + /// + /// Gets the equivalent for the specified + /// . + /// + /// The public-key algorithm. + /// The public-key algorithm. + /// + /// is out of range. + /// + /// + /// does not have an equivalent value. + /// + public static PublicKeyAlgorithm GetPublicKeyAlgorithm (PublicKeyAlgorithmTag algorithm) + { + switch (algorithm) { + case PublicKeyAlgorithmTag.RsaGeneral: return PublicKeyAlgorithm.RsaGeneral; + case PublicKeyAlgorithmTag.RsaEncrypt: return PublicKeyAlgorithm.RsaEncrypt; + case PublicKeyAlgorithmTag.RsaSign: return PublicKeyAlgorithm.RsaSign; + case PublicKeyAlgorithmTag.ElGamalGeneral: return PublicKeyAlgorithm.ElGamalGeneral; + case PublicKeyAlgorithmTag.ElGamalEncrypt: return PublicKeyAlgorithm.ElGamalEncrypt; + case PublicKeyAlgorithmTag.Dsa: return PublicKeyAlgorithm.Dsa; + case PublicKeyAlgorithmTag.ECDH: return PublicKeyAlgorithm.EllipticCurve; + case PublicKeyAlgorithmTag.ECDsa: return PublicKeyAlgorithm.EllipticCurveDsa; + case PublicKeyAlgorithmTag.DiffieHellman: return PublicKeyAlgorithm.DiffieHellman; + default: throw new ArgumentOutOfRangeException (nameof (algorithm)); + } + } + + bool TryGetPublicKey (PgpPublicKeyRing keyring, long keyId, out PgpPublicKey pubkey) + { + if (keyring != null) { + foreach (PgpPublicKey key in keyring.GetPublicKeys ()) { + if (key.KeyId == keyId) { + pubkey = key; + return true; + } + } + } + + pubkey = null; + + return false; + } + + async Task GetDigitalSignaturesAsync (PgpSignatureList signatureList, Stream content, bool doAsync, CancellationToken cancellationToken) + { + var signatures = new List (); + var buf = new byte[4096]; + int nread; + + for (int i = 0; i < signatureList.Count; i++) { + long keyId = signatureList[i].KeyId; + PgpPublicKeyRing keyring; + + if (doAsync) + keyring = await GetPublicKeyRingAsync (keyId, cancellationToken).ConfigureAwait (false); + else + keyring = GetPublicKeyRing (keyId, cancellationToken); + + TryGetPublicKey (keyring, keyId, out var key); + + var signature = new OpenPgpDigitalSignature (keyring, key, signatureList[i]) { + PublicKeyAlgorithm = GetPublicKeyAlgorithm (signatureList[i].KeyAlgorithm), + DigestAlgorithm = GetDigestAlgorithm (signatureList[i].HashAlgorithm), + CreationDate = signatureList[i].CreationTime, + }; + + if (key != null) + signatureList[i].InitVerify (key); + + signatures.Add (signature); + } + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) { + for (int i = 0; i < signatures.Count; i++) { + if (signatures[i].SignerCertificate != null) { + var pgp = (OpenPgpDigitalSignature) signatures[i]; + pgp.Signature.Update (buf, 0, nread); + } + } + } + + return new DigitalSignatureCollection (signatures); + } + + Task VerifyAsync (Stream content, Stream signatureData, bool doAsync, CancellationToken cancellationToken) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + using (var armored = new ArmoredInputStream (signatureData)) { + var factory = new PgpObjectFactory (armored); + var data = factory.NextPgpObject (); + PgpSignatureList signatureList; + + var compressed = data as PgpCompressedData; + if (compressed != null) { + factory = new PgpObjectFactory (compressed.GetDataStream ()); + data = factory.NextPgpObject (); + } + + if (data == null) + throw new FormatException ("Invalid PGP format."); + + signatureList = (PgpSignatureList) data; + + return GetDigitalSignaturesAsync (signatureList, content, doAsync, cancellationToken); + } + } + + /// + /// Verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain valid PGP signature data. + /// + public override DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (content, signatureData, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain valid PGP signature data. + /// + public override Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (content, signatureData, true, cancellationToken); + } + + static Stream Compress (Stream content, byte[] buf) + { + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + var memory = new MemoryBlockStream (); + + using (var compressed = compresser.Open (memory)) { + var literalGenerator = new PgpLiteralDataGenerator (); + + using (var literal = literalGenerator.Open (compressed, 't', "mime.txt", content.Length, DateTime.Now)) { + int nread; + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) + literal.Write (buf, 0, nread); + + literal.Flush (); + } + + compressed.Flush (); + } + + memory.Position = 0; + + return memory; + } + + static Stream Encrypt (PgpEncryptedDataGenerator encrypter, Stream content) + { + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + var buf = new byte[4096]; + + armored.SetHeader ("Version", null); + + using (var compressed = Compress (content, buf)) { + using (var encrypted = encrypter.Open (armored, compressed.Length)) { + int nread; + + while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) + encrypted.Write (buf, 0, nread); + + encrypted.Flush (); + } + } + + armored.Flush (); + } + + memory.Position = 0; + + return memory; + } + + internal static SymmetricKeyAlgorithmTag GetSymmetricKeyAlgorithm (EncryptionAlgorithm algorithm) + { + switch (algorithm) { + case EncryptionAlgorithm.Aes128: return SymmetricKeyAlgorithmTag.Aes128; + case EncryptionAlgorithm.Aes192: return SymmetricKeyAlgorithmTag.Aes192; + case EncryptionAlgorithm.Aes256: return SymmetricKeyAlgorithmTag.Aes256; + case EncryptionAlgorithm.Camellia128: return SymmetricKeyAlgorithmTag.Camellia128; + case EncryptionAlgorithm.Camellia192: return SymmetricKeyAlgorithmTag.Camellia192; + case EncryptionAlgorithm.Camellia256: return SymmetricKeyAlgorithmTag.Camellia256; + case EncryptionAlgorithm.Cast5: return SymmetricKeyAlgorithmTag.Cast5; + case EncryptionAlgorithm.Des: return SymmetricKeyAlgorithmTag.Des; + case EncryptionAlgorithm.TripleDes: return SymmetricKeyAlgorithmTag.TripleDes; + case EncryptionAlgorithm.Idea: return SymmetricKeyAlgorithmTag.Idea; + case EncryptionAlgorithm.Blowfish: return SymmetricKeyAlgorithmTag.Blowfish; + case EncryptionAlgorithm.Twofish: return SymmetricKeyAlgorithmTag.Twofish; + default: throw new NotSupportedException (string.Format ("{0} is not supported.", algorithm)); + } + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// A public key could not be found for one or more of the . + /// + public override MimePart Encrypt (IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + // TODO: document the exceptions that can be thrown by BouncyCastle + return Encrypt (GetPublicKeys (recipients), content); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// A public key could not be found for one or more of the . + /// + /// + /// The specified encryption algorithm is not supported. + /// + public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + // TODO: document the exceptions that can be thrown by BouncyCastle + return Encrypt (algorithm, GetPublicKeys (recipients), content); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (algorithm), true); + var unique = new HashSet (); + int count = 0; + + foreach (var recipient in recipients) { + if (!recipient.IsEncryptionKey) + throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); + + if (unique.Add (recipient.KeyId)) { + encrypter.AddMethod (recipient); + count++; + } + } + + if (count == 0) + throw new ArgumentException ("No recipients specified.", nameof (recipients)); + + var encrypted = Encrypt (encrypter, content); + + return new MimePart ("application", "octet-stream") { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), + Content = new MimeContent (encrypted), + }; + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + public MimePart Encrypt (IEnumerable recipients, Stream content) + { + return Encrypt (defaultAlgorithm, recipients, content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// The private key could not be found for . + /// + /// + /// A public key could not be found for one or more of the . + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return SignAndEncrypt (key, digestAlgo, GetPublicKeys (recipients), content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return SignAndEncrypt (key, digestAlgo, cipherAlgo, GetPublicKeys (recipients), content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (!signer.IsSigningKey) + throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (cipherAlgo), true); + var hashAlgorithm = GetHashAlgorithm (digestAlgo); + var unique = new HashSet (); + var buf = new byte[4096]; + int nread, count = 0; + + foreach (var recipient in recipients) { + if (!recipient.IsEncryptionKey) + throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); + + if (unique.Add (recipient.KeyId)) { + encrypter.AddMethod (recipient); + count++; + } + } + + if (count == 0) + throw new ArgumentException ("No recipients specified.", nameof (recipients)); + + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + + using (var compressed = new MemoryBlockStream ()) { + using (var signed = compresser.Open (compressed)) { + var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); + signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); + var subpacket = new PgpSignatureSubpacketGenerator (); + + foreach (string userId in signer.PublicKey.GetUserIds ()) { + subpacket.SetSignerUserId (false, userId); + break; + } + + signatureGenerator.SetHashedSubpackets (subpacket.Generate ()); + + var onepass = signatureGenerator.GenerateOnePassVersion (false); + onepass.Encode (signed); + + var literalGenerator = new PgpLiteralDataGenerator (); + using (var literal = literalGenerator.Open (signed, 't', "mime.txt", content.Length, DateTime.Now)) { + while ((nread = content.Read (buf, 0, buf.Length)) > 0) { + signatureGenerator.Update (buf, 0, nread); + literal.Write (buf, 0, nread); + } + + literal.Flush (); + } + + var signature = signatureGenerator.Generate (); + signature.Encode (signed); + + signed.Flush (); + } + + compressed.Position = 0; + + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + armored.SetHeader ("Version", null); + + using (var encrypted = encrypter.Open (armored, compressed.Length)) { + while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) + encrypted.Write (buf, 0, nread); + + encrypted.Flush (); + } + + armored.Flush (); + } + + memory.Position = 0; + + return new MimePart ("application", "octet-stream") { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), + Content = new MimeContent (memory) + }; + } + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) + { + return SignAndEncrypt (signer, digestAlgo, defaultAlgorithm, recipients, content); + } + + async Task DecryptToAsync (Stream encryptedData, Stream decryptedData, bool doAsync, CancellationToken cancellationToken) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + if (decryptedData == null) + throw new ArgumentNullException (nameof (decryptedData)); + + using (var armored = new ArmoredInputStream (encryptedData)) { + var factory = new PgpObjectFactory (armored); + var obj = factory.NextPgpObject (); + var list = obj as PgpEncryptedDataList; + + if (list == null) { + // probably a PgpMarker... + obj = factory.NextPgpObject (); + + list = obj as PgpEncryptedDataList; + + if (list == null) + throw new PgpException ("Unexpected OpenPGP packet."); + } + + PgpPublicKeyEncryptedData encrypted = null; + PrivateKeyNotFoundException pkex = null; + bool hasEncryptedPackets = false; + PgpSecretKey secret = null; + + foreach (PgpEncryptedData data in list.GetEncryptedDataObjects ()) { + if ((encrypted = data as PgpPublicKeyEncryptedData) == null) + continue; + + hasEncryptedPackets = true; + + try { + secret = GetSecretKey (encrypted.KeyId); + break; + } catch (PrivateKeyNotFoundException ex) { + pkex = ex; + } + } + + if (!hasEncryptedPackets) + throw new PgpException ("No encrypted packets found."); + + if (secret == null) + throw pkex; + + factory = new PgpObjectFactory (encrypted.GetDataStream (GetPrivateKey (secret))); + List onepassList = null; + DigitalSignatureCollection signatures; + PgpSignatureList signatureList = null; + PgpCompressedData compressed = null; + var position = decryptedData.Position; + long nwritten = 0; + + obj = factory.NextPgpObject (); + while (obj != null) { + if (obj is PgpCompressedData) { + if (compressed != null) + throw new PgpException ("Recursive compression packets are not supported."); + + compressed = (PgpCompressedData) obj; + factory = new PgpObjectFactory (compressed.GetDataStream ()); + } else if (obj is PgpOnePassSignatureList) { + if (nwritten == 0) { + var onepasses = (PgpOnePassSignatureList) obj; + + onepassList = new List (); + + for (int i = 0; i < onepasses.Count; i++) { + var onepass = onepasses[i]; + PgpPublicKeyRing keyring; + + if (doAsync) + keyring = await GetPublicKeyRingAsync (onepass.KeyId, cancellationToken).ConfigureAwait (false); + else + keyring = GetPublicKeyRing (onepass.KeyId, cancellationToken); + + if (!TryGetPublicKey (keyring, onepass.KeyId, out var key)) { + // too messy, pretend we never found a one-pass signature list + onepassList = null; + break; + } + + onepass.InitVerify (key); + + var signature = new OpenPgpDigitalSignature (keyring, key, onepass) { + PublicKeyAlgorithm = GetPublicKeyAlgorithm (onepass.KeyAlgorithm), + DigestAlgorithm = GetDigestAlgorithm (onepass.HashAlgorithm), + }; + + onepassList.Add (signature); + } + } + } else if (obj is PgpSignatureList) { + signatureList = (PgpSignatureList) obj; + } else if (obj is PgpLiteralData) { + var literal = (PgpLiteralData) obj; + + using (var stream = literal.GetDataStream ()) { + var buffer = new byte[4096]; + int nread; + + while ((nread = stream.Read (buffer, 0, buffer.Length)) > 0) { + if (onepassList != null) { + // update our one-pass signatures... + for (int index = 0; index < nread; index++) { + byte c = buffer[index]; + + for (int i = 0; i < onepassList.Count; i++) { + var pgp = (OpenPgpDigitalSignature) onepassList[i]; + pgp.OnePassSignature.Update (c); + } + } + } + + if (doAsync) + await decryptedData.WriteAsync (buffer, 0, nread, cancellationToken).ConfigureAwait (false); + else + decryptedData.Write (buffer, 0, nread); + + nwritten += nread; + } + } + } + + obj = factory.NextPgpObject (); + } + + if (signatureList != null) { + if (onepassList != null && signatureList.Count == onepassList.Count) { + for (int i = 0; i < onepassList.Count; i++) { + var pgp = (OpenPgpDigitalSignature) onepassList[i]; + pgp.CreationDate = signatureList[i].CreationTime; + pgp.Signature = signatureList[i]; + } + + signatures = new DigitalSignatureCollection (onepassList); + } else { + decryptedData.Position = position; + signatures = await GetDigitalSignaturesAsync (signatureList, decryptedData, doAsync, cancellationToken).ConfigureAwait (false); + decryptedData.Position = decryptedData.Length; + } + } else { + signatures = null; + } + + return signatures; + } + } + + /// + /// Decrypt an encrypted stream and extract the digital signers if the content was also signed. + /// + /// + /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. + /// The encrypted data. + /// The stream to write the decrypted data to. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public DigitalSignatureCollection DecryptTo (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + return DecryptToAsync (encryptedData, decryptedData, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously decrypt an encrypted stream and extract the digital signers if the content was also signed. + /// + /// + /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. + /// The encrypted data. + /// The stream to write the decrypted data to. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public Task DecryptToAsync (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + return DecryptToAsync (encryptedData, decryptedData, true, cancellationToken); + } + + /// + /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. + /// + /// + /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. + /// + /// The decrypted . + /// The encrypted data. + /// A list of digital signatures if the data was both signed and encrypted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public MimeEntity Decrypt (Stream encryptedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + using (var decryptedData = new MemoryBlockStream ()) { + signatures = DecryptTo (encryptedData, decryptedData, cancellationToken); + decryptedData.Position = 0; + + return MimeEntity.Load (decryptedData, cancellationToken); + } + } + + /// + /// Decrypts the specified encryptedData. + /// + /// + /// Decrypts the specified encryptedData. + /// + /// The decrypted . + /// The encrypted data. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public override MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + using (var decryptedData = new MemoryBlockStream ()) { + DecryptTo (encryptedData, decryptedData, cancellationToken); + decryptedData.Position = 0; + + return MimeEntity.Load (decryptedData, cancellationToken); + } + } + + /// + /// Import the specified public keyring bundle. + /// + /// + /// Imports the specified public keyring bundle. + /// + /// THe bundle of public keyrings to import. + public abstract void Import (PgpPublicKeyRingBundle bundle); + + /// + /// Releases all resources used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + protected override void Dispose (bool disposing) + { + if (disposing && client != null) { + client.Dispose (); + client = null; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpDataType.cs b/src/MimeKit/Cryptography/OpenPgpDataType.cs new file mode 100644 index 0000000..55cdc30 --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpDataType.cs @@ -0,0 +1,62 @@ +// +// OpenPgpDataType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography +{ + /// + /// An enum expressing a type of OpenPGP data. + /// + /// + /// An enum expressing a type of OpenPGP data. + /// + public enum OpenPgpDataType + { + /// + /// No OpenPGP data detected. + /// + None, + + /// + /// The OpenPGP data is a signed message. + /// + SignedMessage, + + /// + /// The OpenPGP data is an encrypted message. + /// + EncryptedMessage, + + /// + /// The OpenPGP data is a public key. + /// + PublicKey, + + /// + /// The OpenPGP data is a private key. + /// + PrivateKey + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpDetectionFilter.cs b/src/MimeKit/Cryptography/OpenPgpDetectionFilter.cs new file mode 100644 index 0000000..ea9c757 --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpDetectionFilter.cs @@ -0,0 +1,344 @@ +// +// OpenPgpDetectionFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.Utils; +using MimeKit.IO.Filters; + +namespace MimeKit.Cryptography +{ + /// + /// A filter meant to aid in the detection of OpenPGP blocks. + /// + /// + /// Detects OpenPGP block markers and their byte offsets. + /// + public class OpenPgpDetectionFilter : MimeFilterBase + { + enum OpenPgpState { + None = 0, + BeginPgpMessage = (1 << 0), + EndPgpMessage = (1 << 1) | (1 << 0), + BeginPgpSignedMessage = (1 << 2), + BeginPgpSignature = (1 << 3) | (1 << 2), + EndPgpSignature = (1 << 4) | (1 << 3) | (1 << 2), + BeginPgpPublicKeyBlock = (1 << 5), + EndPgpPublicKeyBlock = (1 << 6) | (1 << 5), + BeginPgpPrivateKeyBlock = (1 << 7), + EndPgpPrivateKeyBlock = (1 << 8) | (1 << 7) + } + + struct OpenPgpMarker + { + public byte[] Marker; + public OpenPgpState InitialState; + public OpenPgpState DetectedState; + public bool IsEnd; + + public OpenPgpMarker (string marker, OpenPgpState initial, OpenPgpState detected, bool isEnd) + { + Marker = CharsetUtils.UTF8.GetBytes (marker); + InitialState = initial; + DetectedState = detected; + IsEnd = isEnd; + } + } + + static readonly OpenPgpMarker[] OpenPgpMarkers = { + new OpenPgpMarker ("-----BEGIN PGP MESSAGE-----", OpenPgpState.None, OpenPgpState.BeginPgpMessage, false), + new OpenPgpMarker ("-----END PGP MESSAGE-----", OpenPgpState.BeginPgpMessage, OpenPgpState.EndPgpMessage, true), + new OpenPgpMarker ("-----BEGIN PGP SIGNED MESSAGE-----", OpenPgpState.None, OpenPgpState.BeginPgpSignedMessage, false), + new OpenPgpMarker ("-----BEGIN PGP SIGNATURE-----", OpenPgpState.BeginPgpSignedMessage, OpenPgpState.BeginPgpSignature, false), + new OpenPgpMarker ("-----END PGP SIGNATURE-----", OpenPgpState.BeginPgpSignature, OpenPgpState.EndPgpSignature, true), + new OpenPgpMarker ("-----BEGIN PGP PUBLIC KEY BLOCK-----", OpenPgpState.None, OpenPgpState.BeginPgpPublicKeyBlock, false), + new OpenPgpMarker ("-----END PGP PUBLIC KEY BLOCK-----", OpenPgpState.BeginPgpPublicKeyBlock, OpenPgpState.EndPgpPublicKeyBlock, true), + new OpenPgpMarker ("-----BEGIN PGP PRIVATE KEY BLOCK-----", OpenPgpState.None, OpenPgpState.BeginPgpPrivateKeyBlock, false), + new OpenPgpMarker ("-----END PGP PRIVATE KEY BLOCK-----", OpenPgpState.BeginPgpPrivateKeyBlock, OpenPgpState.EndPgpPrivateKeyBlock, false) + }; + + OpenPgpState state; + int position, next; + bool seenEndMarker; + bool midline; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public OpenPgpDetectionFilter () + { + } + + /// + /// Get the byte offset of the BEGIN marker, if available. + /// + /// + /// Gets the byte offset of the BEGIN marker if available. + /// + /// The byte offset. + public int? BeginOffset { + get; private set; + } + + /// + /// Get the byte offset of the END marker, if available. + /// + /// + /// Gets the byte offset of the END marker if available. + /// + /// The byte offset. + public int? EndOffset { + get; private set; + } + + /// + /// Get the type of OpenPGP data detected. + /// + /// + /// Gets the type of OpenPGP data detected. + /// + /// The type of OpenPGP data detected. + public OpenPgpDataType DataType { + get { + switch (state) { + case OpenPgpState.EndPgpPrivateKeyBlock: return OpenPgpDataType.PrivateKey; + case OpenPgpState.EndPgpPublicKeyBlock: return OpenPgpDataType.PublicKey; + case OpenPgpState.EndPgpSignature: return OpenPgpDataType.SignedMessage; + case OpenPgpState.EndPgpMessage: return OpenPgpDataType.EncryptedMessage; + default: return OpenPgpDataType.None; + } + } + } + + static bool IsMarker (byte[] input, int startIndex, int endIndex, byte[] marker, out bool cr) + { + int i = startIndex; + int j = 0; + + cr = false; + + while (j < marker.Length && i < endIndex) { + if (input[i++] != marker[j++]) + return false; + } + + if (j < marker.Length) + return false; + + if (i < endIndex && input[i] == (byte) '\r') { + cr = true; + i++; + } + + return i < endIndex && input[i] == (byte) '\n'; + } + + static bool IsPartialMatch (byte[] input, int startIndex, int endIndex, byte[] marker) + { + int i = startIndex; + int j = 0; + + while (j < marker.Length && i < endIndex) { + if (input[i++] != marker[j++]) + return false; + } + + if (i < endIndex && input[i] == (byte) '\r') + i++; + + return i == endIndex; + } + + void SetPosition (int offset, int marker, bool cr) + { + int length = OpenPgpMarkers[marker].Marker.Length + (cr ? 2 : 1); + + switch (state) { + case OpenPgpState.BeginPgpPrivateKeyBlock: BeginOffset = position + offset; break; + case OpenPgpState.EndPgpPrivateKeyBlock: EndOffset = position + offset + length; break; + case OpenPgpState.BeginPgpPublicKeyBlock: BeginOffset = position + offset; break; + case OpenPgpState.EndPgpPublicKeyBlock: EndOffset = position + offset + length; break; + case OpenPgpState.BeginPgpSignedMessage: BeginOffset = position + offset; break; + case OpenPgpState.EndPgpSignature: EndOffset = position + offset + length; break; + case OpenPgpState.BeginPgpMessage: BeginOffset = position + offset; break; + case OpenPgpState.EndPgpMessage: EndOffset = position + offset + length; break; + } + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int endIndex = startIndex + length; + int index = startIndex; + bool cr; + + outputIndex = startIndex; + outputLength = 0; + + if (seenEndMarker || length == 0) + return input; + + if (midline) { + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + if (state != OpenPgpState.None) + outputLength = index - startIndex; + + position += index - startIndex; + + return input; + } + + midline = false; + } + + if (state == OpenPgpState.None) { + do { + int lineIndex = index; + + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + bool isPartialMatch = false; + + for (int i = 0; i < OpenPgpMarkers.Length; i++) { + if (OpenPgpMarkers[i].InitialState == state && IsPartialMatch (input, lineIndex, index, OpenPgpMarkers[i].Marker)) { + isPartialMatch = true; + break; + } + } + + if (isPartialMatch) { + SaveRemainingInput (input, lineIndex, index - lineIndex); + position += lineIndex - startIndex; + } else { + position += index - lineIndex; + midline = true; + } + + return input; + } + + index++; + + for (int i = 0; i < OpenPgpMarkers.Length; i++) { + if (OpenPgpMarkers[i].InitialState == state && IsMarker (input, lineIndex, endIndex, OpenPgpMarkers[i].Marker, out cr)) { + state = OpenPgpMarkers[i].DetectedState; + SetPosition (lineIndex - startIndex, i, cr); + outputLength = index - lineIndex; + outputIndex = lineIndex; + next = i + 1; + break; + } + } + } while (index < endIndex && state == OpenPgpState.None); + + if (index == endIndex) { + position += index - startIndex; + return input; + } + } + + do { + int lineIndex = index; + + while (index < endIndex && input[index] != (byte) '\n') + index++; + + if (index == endIndex) { + if (!flush) { + if (IsPartialMatch (input, lineIndex, index, OpenPgpMarkers[next].Marker)) { + SaveRemainingInput (input, lineIndex, index - lineIndex); + outputLength = lineIndex - outputIndex; + position += lineIndex - startIndex; + } else { + outputLength = index - outputIndex; + position += index - startIndex; + midline = true; + } + + return input; + } + + break; + } + + index++; + + if (IsMarker (input, lineIndex, endIndex, OpenPgpMarkers[next].Marker, out cr)) { + seenEndMarker = OpenPgpMarkers[next].IsEnd; + state = OpenPgpMarkers[next].DetectedState; + SetPosition (lineIndex - startIndex, next, cr); + next++; + + if (seenEndMarker) + break; + } + } while (index < endIndex); + + outputLength = index - outputIndex; + position += index - startIndex; + + return input; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + state = OpenPgpState.None; + seenEndMarker = false; + BeginOffset = null; + EndOffset = null; + midline = false; + position = 0; + next = 0; + + base.Reset (); + } + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpDigitalCertificate.cs b/src/MimeKit/Cryptography/OpenPgpDigitalCertificate.cs new file mode 100644 index 0000000..bba0c86 --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpDigitalCertificate.cs @@ -0,0 +1,184 @@ +// +// OpenPgpDigitalCertificate.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using Org.BouncyCastle.Bcpg.OpenPgp; + +namespace MimeKit.Cryptography { + /// + /// An OpenPGP digital certificate. + /// + /// + /// An OpenPGP digital certificate. + /// + public class OpenPgpDigitalCertificate : IDigitalCertificate + { + internal OpenPgpDigitalCertificate (PgpPublicKeyRing keyring, PgpPublicKey pubkey) + { + var bytes = pubkey.GetFingerprint (); + var builder = new StringBuilder (); + + for (int i = 0; i < bytes.Length; i++) + builder.Append (bytes[i].ToString ("X2")); + +// var trust = pubkey.GetTrustData (); +// if (trust != null) { +// TrustLevel = (TrustLevel) (trust[0] & 15); +// } else { +// TrustLevel = TrustLevel.None; +// } + + Fingerprint = builder.ToString (); + PublicKey = pubkey; + KeyRing = keyring; + + if (!UpdateUserId (pubkey) && !pubkey.IsMasterKey) { + foreach (PgpPublicKey key in keyring.GetPublicKeys ()) { + if (key.IsMasterKey) { + UpdateUserId (key); + break; + } + } + } + } + + bool UpdateUserId (PgpPublicKey pubkey) + { + foreach (string userId in pubkey.GetUserIds ()) { + var bytes = Encoding.UTF8.GetBytes (userId); + MailboxAddress mailbox; + int index = 0; + + if (!MailboxAddress.TryParse (ParserOptions.Default, bytes, ref index, bytes.Length, false, out mailbox)) + continue; + + Email = mailbox.Address; + Name = mailbox.Name; + return true; + } + + return false; + } + + /// + /// Gets the public key ring. + /// + /// + /// Get the public key ring that is associated with. + /// + /// The key ring. + public PgpPublicKeyRing KeyRing { + get; private set; + } + + /// + /// Gets the public key. + /// + /// + /// Get the public key. + /// + /// The public key. + public PgpPublicKey PublicKey { + get; private set; + } + + #region IDigitalCertificate implementation + + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get { return OpenPgpContext.GetPublicKeyAlgorithm (PublicKey.Algorithm); } + } + + /// + /// Gets the date that the certificate was created. + /// + /// + /// Gets the date that the certificate was created. + /// + /// The creation date. + public DateTime CreationDate { + get { return PublicKey.CreationTime; } + } + + /// + /// Gets the expiration date of the certificate. + /// + /// + /// Gets the expiration date of the certificate. + /// + /// The expiration date. + public DateTime ExpirationDate { + get { + long seconds = PublicKey.GetValidSeconds (); + + return seconds > 0 ? CreationDate.AddSeconds ((double) seconds) : DateTime.MaxValue; + } + } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// Gets the fingerprint of the certificate. + /// + /// The fingerprint. + public string Fingerprint { + get; private set; + } + + /// + /// Gets the email address of the owner of the certificate. + /// + /// + /// Gets the email address of the owner of the certificate. + /// + /// The email address. + public string Email { + get; private set; + } + + /// + /// Gets the name of the owner of the certificate. + /// + /// + /// Gets the name of the owner of the certificate. + /// + /// The name of the owner. + public string Name { + get; private set; + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpDigitalSignature.cs b/src/MimeKit/Cryptography/OpenPgpDigitalSignature.cs new file mode 100644 index 0000000..63073ff --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpDigitalSignature.cs @@ -0,0 +1,164 @@ +// +// OpenPgpDigitalSignature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using Org.BouncyCastle.Bcpg.OpenPgp; + +namespace MimeKit.Cryptography { + /// + /// An OpenPGP digital signature. + /// + /// + /// An OpenPGP digital signature. + /// + public class OpenPgpDigitalSignature : IDigitalSignature + { + DigitalSignatureVerifyException vex; + bool? valid; + + internal OpenPgpDigitalSignature (PgpPublicKeyRing keyring, PgpPublicKey pubkey, PgpOnePassSignature signature) + { + SignerCertificate = pubkey != null ? new OpenPgpDigitalCertificate (keyring, pubkey) : null; + OnePassSignature = signature; + } + + internal OpenPgpDigitalSignature (PgpPublicKeyRing keyring, PgpPublicKey pubkey, PgpSignature signature) + { + SignerCertificate = pubkey != null ? new OpenPgpDigitalCertificate (keyring, pubkey) : null; + Signature = signature; + } + + internal PgpOnePassSignature OnePassSignature { + get; private set; + } + + internal PgpSignature Signature { + get; set; + } + + #region IDigitalSignature implementation + + /// + /// Gets certificate used by the signer. + /// + /// + /// Gets certificate used by the signer. + /// + /// The signer's certificate. + public IDigitalCertificate SignerCertificate { + get; private set; + } + + /// + /// Gets the public key algorithm used for the signature. + /// + /// + /// Gets the public key algorithm used for the signature. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get; internal set; + } + + /// + /// Gets the digest algorithm used for the signature. + /// + /// + /// Gets the digest algorithm used for the signature. + /// + /// The digest algorithm. + public DigestAlgorithm DigestAlgorithm { + get; internal set; + } + + /// + /// Gets the creation date of the digital signature. + /// + /// + /// Gets the creation date of the digital signature. + /// + /// The creation date. + public DateTime CreationDate { + get; internal set; + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify () + { + if (valid.HasValue) + return valid.Value; + + if (vex != null) + throw vex; + + if (SignerCertificate == null) { + var message = string.Format ("Failed to verify digital signature: no public key found for {0:X8}", (int) Signature.KeyId); + vex = new DigitalSignatureVerifyException (Signature.KeyId, message); + throw vex; + } + + try { + if (OnePassSignature != null) + valid = OnePassSignature.Verify (Signature); + else + valid = Signature.Verify (); + return valid.Value; + } catch (Exception ex) { + var message = string.Format ("Failed to verify digital signature: {0}", ex.Message); + vex = new DigitalSignatureVerifyException (Signature.KeyId, message, ex); + throw vex; + } + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// This option is ignored for OpenPGP digital signatures. + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify (bool verifySignatureOnly) + { + return Verify (); + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/OpenPgpKeyCertification.cs b/src/MimeKit/Cryptography/OpenPgpKeyCertification.cs new file mode 100644 index 0000000..656315b --- /dev/null +++ b/src/MimeKit/Cryptography/OpenPgpKeyCertification.cs @@ -0,0 +1,65 @@ +// +// OpenPgpKeyCertification.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography +{ + /// + /// An OpenPGP key certification. + /// + /// + /// An OpenPGP key certification. + /// + public enum OpenPgpKeyCertification { + /// + /// Generic certification of a User ID and Public-Key packet. + /// The issuer of this certification does not make any particular + /// assertion as to how well the certifier has checked that the owner + /// of the key is in fact the person described by the User ID. + /// + GenericCertification = 0x10, + + /// + /// Persona certification of a User ID and Public-Key packet. + /// The issuer of this certification has not done any verification of + /// the claim that the owner of this key is the User ID specified. + /// + PersonaCertification = 0x11, + + /// + /// Casual certification of a User ID and Public-Key packet. + /// The issuer of this certification has done some casual + /// verification of the claim of identity. + /// + CasualCertification = 0x12, + + /// + /// Positive certification of a User ID and Public-Key packet. + /// The issuer of this certification has done substantial + /// verification of the claim of identity. + /// + PositiveCertification = 0x13 + } +} diff --git a/src/MimeKit/Cryptography/PrivateKeyNotFoundException.cs b/src/MimeKit/Cryptography/PrivateKeyNotFoundException.cs new file mode 100644 index 0000000..b4bf7bb --- /dev/null +++ b/src/MimeKit/Cryptography/PrivateKeyNotFoundException.cs @@ -0,0 +1,152 @@ +// +// PrivateKeyNotFoundException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MimeKit.Cryptography { + /// + /// An exception that is thrown when a private key could not be found for a specified mailbox or key id. + /// + /// + /// An exception that is thrown when a private key could not be found for a specified mailbox or key id. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class PrivateKeyNotFoundException : Exception + { +#if SERIALIZABLE + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The stream context. + /// + /// is null. + /// + protected PrivateKeyNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) + { + KeyId = info.GetString ("KeyId"); + } +#endif + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The mailbox that could not be resolved to a valid private key. + /// A message explaining the error. + /// + /// is null. + /// + public PrivateKeyNotFoundException (MailboxAddress mailbox, string message) : base (message) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + KeyId = mailbox.Address; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The key id that could not be resolved to a valid certificate. + /// A message explaining the error. + /// + /// is null. + /// + public PrivateKeyNotFoundException (string keyid, string message) : base (message) + { + if (keyid == null) + throw new ArgumentNullException (nameof (keyid)); + + KeyId = keyid; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The key id that could not be resolved to a valid certificate. + /// A message explaining the error. + /// + /// is null. + /// + public PrivateKeyNotFoundException (long keyid, string message) : base (message) + { + KeyId = keyid.ToString ("X"); + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("KeyId", KeyId); + } +#endif + + /// + /// Gets the key id that could not be found. + /// + /// + /// Gets the key id that could not be found. + /// + /// The key id. + public string KeyId { + get; private set; + } + } +} diff --git a/src/MimeKit/Cryptography/PublicKeyAlgorithm.cs b/src/MimeKit/Cryptography/PublicKeyAlgorithm.cs new file mode 100644 index 0000000..8ece4b6 --- /dev/null +++ b/src/MimeKit/Cryptography/PublicKeyAlgorithm.cs @@ -0,0 +1,90 @@ +// +// PublicKeyAlgorithm.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// An enumeration of public key algorithms. + /// + /// + /// An enumeration of public key algorithms. + /// + public enum PublicKeyAlgorithm { + /// + /// No public key algorithm specified. + /// + None = 0, + + /// + /// The RSA algorithm. + /// + RsaGeneral = 1, + + /// + /// The RSA encryption-only algorithm. + /// + RsaEncrypt = 2, + + /// + /// The RSA sign-only algorithm. + /// + RsaSign = 3, + + /// + /// The El-Gamal encryption-only algorithm. + /// + ElGamalEncrypt = 16, + + /// + /// The DSA algorithm. + /// + Dsa = 17, + + /// + /// The elliptic curve algorithm (aka EC or ECDH). + /// + EllipticCurve = 18, + + /// + /// The elliptic curve DSA algorithm (aka ECDSA). + /// + EllipticCurveDsa = 19, + + /// + /// The El-Gamal algorithm. + /// + ElGamalGeneral = 20, + + /// + /// The Diffie-Hellman algorithm. + /// + DiffieHellman = 21, + + /// + /// The Edwards-Curve DSA algorithm (aka EdDSA). + /// + EdwardsCurveDsa = 22 + } +} diff --git a/src/MimeKit/Cryptography/PublicKeyNotFoundException.cs b/src/MimeKit/Cryptography/PublicKeyNotFoundException.cs new file mode 100644 index 0000000..2221f8f --- /dev/null +++ b/src/MimeKit/Cryptography/PublicKeyNotFoundException.cs @@ -0,0 +1,115 @@ +// +// PublicKeyNotFoundException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MimeKit.Cryptography { + /// + /// An exception that is thrown when a public key could not be found for a specified mailbox. + /// + /// + /// An exception that is thrown when a public key could not be found for a specified mailbox. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class PublicKeyNotFoundException : Exception + { +#if SERIALIZABLE + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The stream context. + /// + /// is null. + /// + protected PublicKeyNotFoundException (SerializationInfo info, StreamingContext context) : base (info, context) + { + var text = info.GetString ("Mailbox"); + MailboxAddress mailbox; + + if (MailboxAddress.TryParse (text, out mailbox)) + Mailbox = mailbox; + } +#endif + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The mailbox that could not be resolved to a valid private key. + /// A message explaining the error. + public PublicKeyNotFoundException (MailboxAddress mailbox, string message) : base (message) + { + Mailbox = mailbox; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("Mailbox", Mailbox.ToString (true)); + } +#endif + + /// + /// Gets the key id that could not be found. + /// + /// + /// Gets the key id that could not be found. + /// + /// The key id. + public MailboxAddress Mailbox { + get; private set; + } + } +} diff --git a/src/MimeKit/Cryptography/RsaEncryptionPadding.cs b/src/MimeKit/Cryptography/RsaEncryptionPadding.cs new file mode 100644 index 0000000..e009605 --- /dev/null +++ b/src/MimeKit/Cryptography/RsaEncryptionPadding.cs @@ -0,0 +1,251 @@ +// +// RsaEncryptionPadding.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if NETCOREAPP3_0 +using System.Security.Cryptography; +#endif + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; + +namespace MimeKit.Cryptography { + /// + /// The RSA encryption padding schemes and parameters used by S/MIME. + /// + /// + /// The RSA encryption padding schemes and parameters used by S/MIME as described in + /// rfc8017. + /// + public sealed class RsaEncryptionPadding : IEquatable + { + /// + /// The PKCS #1 v1.5 encryption padding. + /// + public static readonly RsaEncryptionPadding Pkcs1 = new RsaEncryptionPadding (RsaEncryptionPaddingScheme.Pkcs1, DigestAlgorithm.None); + + /// + /// The Optimal Asymmetric Encryption Padding (OAEP) scheme using the default (SHA-1) hash algorithm. + /// + public static readonly RsaEncryptionPadding OaepSha1 = new RsaEncryptionPadding (RsaEncryptionPaddingScheme.Oaep, DigestAlgorithm.Sha1); + + /// + /// The Optimal Asymmetric Encryption Padding (OAEP) scheme using the SHA-256 hash algorithm. + /// + public static readonly RsaEncryptionPadding OaepSha256 = new RsaEncryptionPadding (RsaEncryptionPaddingScheme.Oaep, DigestAlgorithm.Sha256); + + /// + /// The Optimal Asymmetric Encryption Padding (OAEP) scheme using the SHA-384 hash algorithm. + /// + public static readonly RsaEncryptionPadding OaepSha384 = new RsaEncryptionPadding (RsaEncryptionPaddingScheme.Oaep, DigestAlgorithm.Sha384); + + /// + /// The Optimal Asymmetric Encryption Padding (OAEP) scheme using the SHA-512 hash algorithm. + /// + public static readonly RsaEncryptionPadding OaepSha512 = new RsaEncryptionPadding (RsaEncryptionPaddingScheme.Oaep, DigestAlgorithm.Sha512); + + RsaEncryptionPadding (RsaEncryptionPaddingScheme scheme, DigestAlgorithm oaepHashAlgorithm) + { + OaepHashAlgorithm = oaepHashAlgorithm; + Scheme = scheme; + } + + /// + /// Get the RSA encryption padding scheme. + /// + /// + /// Gets the RSA encryption padding scheme. + /// + /// The RSA encryption padding scheme. + public RsaEncryptionPaddingScheme Scheme { + get; private set; + } + + /// + /// Get the hash algorithm used for RSAES-OAEP padding. + /// + /// + /// Gets the hash algorithm used for RSAES-OAEP padding. + /// + public DigestAlgorithm OaepHashAlgorithm { + get; private set; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Compares two RSA encryption paddings to determine if they are identical or not. + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (RsaEncryptionPadding other) + { + if (other == null) + return false; + + return other.Scheme == Scheme && other.OaepHashAlgorithm == OaepHashAlgorithm; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// The type of comparison between the current instance and the parameter depends on whether + /// the current instance is a reference type or a value type. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals (object obj) + { + return Equals (obj as RsaEncryptionPadding); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// Returns the hash code for this instance. + /// + /// A hash code for the current object. + public override int GetHashCode () + { + int hash = Scheme.GetHashCode (); + + return ((hash << 5) + hash) ^ OaepHashAlgorithm.GetHashCode (); + } + + /// + /// Returns a that represents the current + /// . + /// + /// + /// Creates a string-representation of the . + /// + /// A that represents the current + /// . + public override string ToString () + { + return Scheme == RsaEncryptionPaddingScheme.Pkcs1 ? "Pkcs1" : "Oaep" + OaepHashAlgorithm.ToString (); + } + + /// + /// Compare two objects for equality. + /// + /// + /// Compares two objects for equality. + /// + /// The first object to compare. + /// The second object to compare. + /// true if and are equal; otherwise, false. + public static bool operator == (RsaEncryptionPadding left, RsaEncryptionPadding right) + { + if (ReferenceEquals (left, null)) + return ReferenceEquals (right, null); + + return left.Equals (right); + } + + /// + /// Compare two objects for inequality. + /// + /// + /// Compares two objects for inequality. + /// + /// The first object to compare. + /// The second object to compare. + /// true if and are unequal; otherwise, false. + public static bool operator != (RsaEncryptionPadding left, RsaEncryptionPadding right) + { + return !(left == right); + } + + /// + /// Create a new using and the specified hash algorithm. + /// + /// + /// Creates a new using and the specified hash algorithm. + /// + /// The hash algorithm. + /// An using and the specified hash algorithm. + /// + /// The is not supported. + /// + public static RsaEncryptionPadding CreateOaep (DigestAlgorithm hashAlgorithm) + { + switch (hashAlgorithm) { + case DigestAlgorithm.Sha1: return OaepSha1; + case DigestAlgorithm.Sha256: return OaepSha256; + case DigestAlgorithm.Sha384: return OaepSha384; + case DigestAlgorithm.Sha512: return OaepSha512; + default: throw new NotSupportedException ($"The {hashAlgorithm} hash algorithm is not supported."); + } + } + + internal RsaesOaepParameters GetRsaesOaepParameters () + { + if (OaepHashAlgorithm == DigestAlgorithm.Sha1) + return new RsaesOaepParameters (); + + var oid = SecureMimeContext.GetDigestOid (OaepHashAlgorithm); + var hashAlgorithm = new AlgorithmIdentifier (new DerObjectIdentifier (oid), DerNull.Instance); + var maskGenFunction = new AlgorithmIdentifier (PkcsObjectIdentifiers.IdMgf1, hashAlgorithm); + + return new RsaesOaepParameters (hashAlgorithm, maskGenFunction, RsaesOaepParameters.DefaultPSourceAlgorithm); + } + + internal AlgorithmIdentifier GetAlgorithmIdentifier () + { + if (Scheme != RsaEncryptionPaddingScheme.Oaep) + return null; + + return new AlgorithmIdentifier (PkcsObjectIdentifiers.IdRsaesOaep, GetRsaesOaepParameters ()); + } + +#if NETCOREAPP3_0 + internal RSAEncryptionPadding AsRSAEncryptionPadding () + { + switch (Scheme) { + case RsaEncryptionPaddingScheme.Oaep: + switch (OaepHashAlgorithm) { + case DigestAlgorithm.Sha1: return RSAEncryptionPadding.OaepSHA1; + case DigestAlgorithm.Sha256: return RSAEncryptionPadding.OaepSHA256; + case DigestAlgorithm.Sha384: return RSAEncryptionPadding.OaepSHA384; + case DigestAlgorithm.Sha512: return RSAEncryptionPadding.OaepSHA512; + default: return null; + } + case RsaEncryptionPaddingScheme.Pkcs1: + return RSAEncryptionPadding.Pkcs1; + default: + return null; + } + } +#endif + } +} diff --git a/src/MimeKit/Cryptography/RsaEncryptionPaddingScheme.cs b/src/MimeKit/Cryptography/RsaEncryptionPaddingScheme.cs new file mode 100644 index 0000000..c867bc3 --- /dev/null +++ b/src/MimeKit/Cryptography/RsaEncryptionPaddingScheme.cs @@ -0,0 +1,47 @@ +// +// RsaEncryptionPaddingScheme.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// The RSA encryption padding schemes used by S/MIME. + /// + /// + /// The RSA encryption padding schemes used by S/MIME as described in + /// rfc8017. + /// + public enum RsaEncryptionPaddingScheme + { + /// + /// The PKCS #1 v1.5 encryption padding scheme. + /// + Pkcs1, + + /// + /// The Optimal Asymmetric Encryption Padding (OAEP) scheme. + /// + Oaep + } +} diff --git a/src/MimeKit/Cryptography/RsaSignaturePadding.cs b/src/MimeKit/Cryptography/RsaSignaturePadding.cs new file mode 100644 index 0000000..dce3132 --- /dev/null +++ b/src/MimeKit/Cryptography/RsaSignaturePadding.cs @@ -0,0 +1,153 @@ +// +// RsaSignaturePadding.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Cryptography { + /// + /// The RSA signature padding schemes and parameters used by S/MIME. + /// + /// + /// The RSA signature padding schemes and parameters used by S/MIME as described in + /// rfc8017. + /// + public sealed class RsaSignaturePadding : IEquatable + { + /// + /// The PKCS #1 v1.5 signature padding. + /// + public static readonly RsaSignaturePadding Pkcs1 = new RsaSignaturePadding (RsaSignaturePaddingScheme.Pkcs1); + + /// + /// The Probibilistic Signature Scheme (PSS) padding. + /// + public static readonly RsaSignaturePadding Pss = new RsaSignaturePadding (RsaSignaturePaddingScheme.Pss); + + RsaSignaturePadding (RsaSignaturePaddingScheme scheme) + { + Scheme = scheme; + } + + /// + /// Get the RSA signature padding scheme. + /// + /// + /// Gets the RSA signature padding scheme. + /// + /// The RSA signature padding scheme. + public RsaSignaturePaddingScheme Scheme { + get; private set; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Compares two RSA Signature paddings to determine if they are identical or not. + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (RsaSignaturePadding other) + { + if (other == null) + return false; + + return other.Scheme == Scheme; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// The type of comparison between the current instance and the parameter depends on whether + /// the current instance is a reference type or a value type. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals (object obj) + { + return Equals (obj as RsaSignaturePadding); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// Returns the hash code for this instance. + /// + /// A hash code for the current object. + public override int GetHashCode () + { + return Scheme.GetHashCode (); + } + + /// + /// Returns a that represents the current + /// . + /// + /// + /// Creates a string-representation of the . + /// + /// A that represents the current + /// . + public override string ToString () + { + return Scheme == RsaSignaturePaddingScheme.Pkcs1 ? "Pkcs1" : "Pss"; + } + + /// + /// Compare two objects for equality. + /// + /// + /// Compares two objects for equality. + /// + /// The first object to compare. + /// The second object to compare. + /// true if and are equal; otherwise, false. + public static bool operator == (RsaSignaturePadding left, RsaSignaturePadding right) + { + if (ReferenceEquals (left, null)) + return ReferenceEquals (right, null); + + return left.Equals (right); + } + + /// + /// Compare two objects for inequality. + /// + /// + /// Compares two objects for inequality. + /// + /// The first object to compare. + /// The second object to compare. + /// true if and are unequal; otherwise, false. + public static bool operator != (RsaSignaturePadding left, RsaSignaturePadding right) + { + return !(left == right); + } + } +} diff --git a/src/MimeKit/Cryptography/RsaSignaturePaddingScheme.cs b/src/MimeKit/Cryptography/RsaSignaturePaddingScheme.cs new file mode 100644 index 0000000..c408c47 --- /dev/null +++ b/src/MimeKit/Cryptography/RsaSignaturePaddingScheme.cs @@ -0,0 +1,47 @@ +// +// RsaSignaturePaddingScheme.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// The RSA signature padding schemes used by S/MIME. + /// + /// + /// The RSA signature padding schemes used by S/MIME as described in + /// rfc8017. + /// + public enum RsaSignaturePaddingScheme + { + /// + /// The PKCS #1 v1.5 signature padding scheme. + /// + Pkcs1, + + /// + /// The Probibilistic Signature Scheme (PSS). + /// + Pss + } +} diff --git a/src/MimeKit/Cryptography/SecureMailboxAddress.cs b/src/MimeKit/Cryptography/SecureMailboxAddress.cs new file mode 100644 index 0000000..c93bd8d --- /dev/null +++ b/src/MimeKit/Cryptography/SecureMailboxAddress.cs @@ -0,0 +1,219 @@ +// +// SecureMailboxAddress.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// A secure mailbox address which includes a fingerprint for a certificate. + /// + /// + /// When signing or encrypting a message, it is necessary to look up the + /// X.509 certificate in order to do the actual sign or encrypt operation. One + /// way of accomplishing this is to use the email address of sender or recipient + /// as a unique identifier. However, a better approach is to use the fingerprint + /// (or 'thumbprint' in Microsoft parlance) of the user's certificate. + /// + public class SecureMailboxAddress : MailboxAddress + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified fingerprint. + /// + /// The character encoding to be used for encoding the name. + /// The name of the mailbox. + /// The route of the mailbox. + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public SecureMailboxAddress (Encoding encoding, string name, IEnumerable route, string address, string fingerprint) : base (encoding, name, route, address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified fingerprint. + /// + /// The name of the mailbox. + /// The route of the mailbox. + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public SecureMailboxAddress (string name, IEnumerable route, string address, string fingerprint) : base (name, route, address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified fingerprint. + /// + /// The route of the mailbox. + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use new SecureMailboxAddress (string.Empty, route, address, fingerprint) instead.")] + public SecureMailboxAddress (IEnumerable route, string address, string fingerprint) : base (route, address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified fingerprint. + /// + /// The character encoding to be used for encoding the name. + /// The name of the mailbox. + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public SecureMailboxAddress (Encoding encoding, string name, string address, string fingerprint) : base (encoding, name, address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified fingerprint. + /// + /// The name of the mailbox. + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// + public SecureMailboxAddress (string name, string address, string fingerprint) : base (name, address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified address. + /// + /// The must be in the form user@example.com. + /// This method cannot be used to parse a free-form email address that includes + /// the name or encloses the address in angle brackets. + /// To parse a free-form email address, use + /// instead. + /// + /// + /// The address of the mailbox. + /// The fingerprint of the certificate belonging to the owner of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// + [Obsolete ("Use new SecureMailboxAddress (string.Empty, address, fingerprint) instead.")] + public SecureMailboxAddress (string address, string fingerprint) : base (address) + { + ValidateFingerprint (fingerprint); + + Fingerprint = fingerprint; + } + + static void ValidateFingerprint (string fingerprint) + { + if (fingerprint == null) + throw new ArgumentNullException (nameof (fingerprint)); + + for (int i = 0; i < fingerprint.Length; i++) { + if (fingerprint[i] > 128 || !((byte) fingerprint[i]).IsXDigit ()) + throw new ArgumentException ("The fingerprint should be a hex-encoded string.", nameof (fingerprint)); + } + } + + /// + /// Gets the fingerprint of the certificate and/or key to use for signing or encrypting. + /// + /// + /// A fingerprint is a SHA-1 hash of the raw certificate data and is often used + /// as a unique identifier for a particular certificate in a certificate store. + /// + /// + /// + /// The fingerprint of the certificate. + public string Fingerprint { + get; private set; + } + } +} diff --git a/src/MimeKit/Cryptography/SecureMimeContext.cs b/src/MimeKit/Cryptography/SecureMimeContext.cs new file mode 100644 index 0000000..2aa2978 --- /dev/null +++ b/src/MimeKit/Cryptography/SecureMimeContext.cs @@ -0,0 +1,842 @@ +// +// SecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; + +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Asn1.Ntt; +using Org.BouncyCastle.Asn1.Kisa; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.Smime; + +namespace MimeKit.Cryptography { + /// + /// A Secure MIME (S/MIME) cryptography context. + /// + /// + /// Generally speaking, applications should not use a + /// directly, but rather via higher level APIs such as + /// and . + /// + public abstract class SecureMimeContext : CryptographyContext + { + static readonly string[] ProtocolSubtypes = { "pkcs7-signature", "pkcs7-mime", "pkcs7-keys", "x-pkcs7-signature", "x-pkcs7-mime", "x-pkcs7-keys" }; + internal const X509KeyUsageFlags DigitalSignatureKeyUsageFlags = X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation; + internal static readonly int EncryptionAlgorithmCount = Enum.GetValues (typeof (EncryptionAlgorithm)).Length; + internal static readonly DerObjectIdentifier Blowfish = new DerObjectIdentifier ("1.3.6.1.4.1.3029.1.2"); + internal static readonly DerObjectIdentifier Twofish = new DerObjectIdentifier ("1.3.6.1.4.1.25258.3.3"); + + /// + /// Initialize a new instance of the class. + /// + /// + /// Enables the following encryption algorithms by default: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + protected SecureMimeContext () + { + EncryptionAlgorithmRank = new[] { + EncryptionAlgorithm.Aes256, + EncryptionAlgorithm.Aes192, + EncryptionAlgorithm.Aes128, + //EncryptionAlgorithm.Twofish, + EncryptionAlgorithm.Seed, + EncryptionAlgorithm.Camellia256, + EncryptionAlgorithm.Camellia192, + EncryptionAlgorithm.Camellia128, + EncryptionAlgorithm.Cast5, + EncryptionAlgorithm.Blowfish, + EncryptionAlgorithm.TripleDes, + EncryptionAlgorithm.Idea, + EncryptionAlgorithm.RC2128, + EncryptionAlgorithm.RC264, + EncryptionAlgorithm.Des, + EncryptionAlgorithm.RC240 + }; + + foreach (var algorithm in EncryptionAlgorithmRank) { + Enable (algorithm); + + // Don't enable anything weaker than Triple-DES by default + if (algorithm == EncryptionAlgorithm.TripleDes) + break; + } + + // Disable Blowfish and Twofish by default for now + Disable (EncryptionAlgorithm.Blowfish); + Disable (EncryptionAlgorithm.Twofish); + + // TODO: Set a preferred digest algorithm rank and enable them. + } + + /// + /// Get the signature protocol. + /// + /// + /// The signature protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The signature protocol. + public override string SignatureProtocol { + get { return "application/pkcs7-signature"; } + } + + /// + /// Get the encryption protocol. + /// + /// + /// The encryption protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The encryption protocol. + public override string EncryptionProtocol { + get { return "application/pkcs7-mime"; } + } + + /// + /// Get the key exchange protocol. + /// + /// + /// Gets the key exchange protocol. + /// + /// The key exchange protocol. + public override string KeyExchangeProtocol { + get { return "application/pkcs7-mime"; } + } + + /// + /// Check whether or not the specified protocol is supported by the . + /// + /// + /// Used in order to make sure that the protocol parameter value specified in either a multipart/signed + /// or multipart/encrypted part is supported by the supplied cryptography context. + /// + /// true if the protocol is supported; otherwise false + /// The protocol. + /// + /// is null. + /// + public override bool Supports (string protocol) + { + if (protocol == null) + throw new ArgumentNullException (nameof (protocol)); + + if (!protocol.StartsWith ("application/", StringComparison.OrdinalIgnoreCase)) + return false; + + int startIndex = "application/".Length; + int subtypeLength = protocol.Length - startIndex; + + for (int i = 0; i < ProtocolSubtypes.Length; i++) { + if (subtypeLength != ProtocolSubtypes[i].Length) + continue; + + if (string.Compare (protocol, startIndex, ProtocolSubtypes[i], 0, subtypeLength, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + + return false; + } + + /// + /// Get the string name of the digest algorithm for use with the micalg parameter of a multipart/signed part. + /// + /// + /// Maps the to the appropriate string identifier + /// as used by the micalg parameter value of a multipart/signed Content-Type + /// header. For example: + /// + /// AlgorithmName + /// md2 + /// md4 + /// md5 + /// sha-1 + /// sha-224 + /// sha-256 + /// sha-384 + /// sha-512 + /// tiger-192 + /// ripemd160 + /// haval-5-160 + /// + /// + /// The micalg value. + /// The digest algorithm. + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + public override string GetDigestAlgorithmName (DigestAlgorithm micalg) + { + switch (micalg) { + case DigestAlgorithm.MD5: return "md5"; + case DigestAlgorithm.Sha1: return "sha-1"; + case DigestAlgorithm.RipeMD160: return "ripemd160"; + case DigestAlgorithm.MD2: return "md2"; + case DigestAlgorithm.Tiger192: return "tiger192"; + case DigestAlgorithm.Haval5160: return "haval-5-160"; + case DigestAlgorithm.Sha256: return "sha-256"; + case DigestAlgorithm.Sha384: return "sha-384"; + case DigestAlgorithm.Sha512: return "sha-512"; + case DigestAlgorithm.Sha224: return "sha-224"; + case DigestAlgorithm.MD4: return "md4"; + case DigestAlgorithm.DoubleSha: + throw new NotSupportedException (string.Format ("{0} is not supported.", micalg)); + default: + throw new ArgumentOutOfRangeException (nameof (micalg), micalg, string.Format ("Unknown DigestAlgorithm: {0}", micalg)); + } + } + + /// + /// Get the digest algorithm from the micalg parameter value in a multipart/signed part. + /// + /// + /// Maps the micalg parameter value string back to the appropriate . + /// Maps the micalg parameter value string back to the appropriate + /// + /// AlgorithmName + /// md2 + /// md4 + /// md5 + /// sha-1 + /// sha-224 + /// sha-256 + /// sha-384 + /// sha-512 + /// tiger-192 + /// ripemd160 + /// haval-5-160 + /// + /// + /// The digest algorithm. + /// The micalg parameter value. + /// + /// is null. + /// + public override DigestAlgorithm GetDigestAlgorithm (string micalg) + { + if (micalg == null) + throw new ArgumentNullException (nameof (micalg)); + + switch (micalg.ToLowerInvariant ()) { + case "md5": return DigestAlgorithm.MD5; + case "sha-1": return DigestAlgorithm.Sha1; + case "ripemd160": return DigestAlgorithm.RipeMD160; + case "md2": return DigestAlgorithm.MD2; + case "tiger192": return DigestAlgorithm.Tiger192; + case "haval-5-160": return DigestAlgorithm.Haval5160; + case "sha-256": return DigestAlgorithm.Sha256; + case "sha-384": return DigestAlgorithm.Sha384; + case "sha-512": return DigestAlgorithm.Sha512; + case "sha-224": return DigestAlgorithm.Sha224; + case "md4": return DigestAlgorithm.MD4; + default: return DigestAlgorithm.None; + } + } + + /// + /// Get the OID for the digest algorithm. + /// + /// + /// Gets the OID for the digest algorithm. + /// + /// The digest oid. + /// The digest algorithm. + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + internal protected static string GetDigestOid (DigestAlgorithm digestAlgo) + { + switch (digestAlgo) { + case DigestAlgorithm.MD5: return CmsSignedGenerator.DigestMD5; + case DigestAlgorithm.Sha1: return CmsSignedGenerator.DigestSha1; + case DigestAlgorithm.MD2: return PkcsObjectIdentifiers.MD2.Id; + case DigestAlgorithm.Sha256: return CmsSignedGenerator.DigestSha256; + case DigestAlgorithm.Sha384: return CmsSignedGenerator.DigestSha384; + case DigestAlgorithm.Sha512: return CmsSignedGenerator.DigestSha512; + case DigestAlgorithm.Sha224: return CmsSignedGenerator.DigestSha224; + case DigestAlgorithm.MD4: return PkcsObjectIdentifiers.MD4.Id; + case DigestAlgorithm.RipeMD160: return CmsSignedGenerator.DigestRipeMD160; + case DigestAlgorithm.DoubleSha: + case DigestAlgorithm.Tiger192: + case DigestAlgorithm.Haval5160: + throw new NotSupportedException (string.Format ("{0} is not supported.", digestAlgo)); + default: + throw new ArgumentOutOfRangeException (nameof (digestAlgo), digestAlgo, string.Format ("Unknown DigestAlgorithm: {0}", digestAlgo)); + } + } + + internal static bool TryGetDigestAlgorithm (string id, out DigestAlgorithm algorithm) + { + if (id == CmsSignedGenerator.DigestSha1) { + algorithm = DigestAlgorithm.Sha1; + return true; + } + + if (id == CmsSignedGenerator.DigestSha224) { + algorithm = DigestAlgorithm.Sha224; + return true; + } + + if (id == CmsSignedGenerator.DigestSha256) { + algorithm = DigestAlgorithm.Sha256; + return true; + } + + if (id == CmsSignedGenerator.DigestSha384) { + algorithm = DigestAlgorithm.Sha384; + return true; + } + + if (id == CmsSignedGenerator.DigestSha512) { + algorithm = DigestAlgorithm.Sha512; + return true; + } + + if (id == CmsSignedGenerator.DigestRipeMD160) { + algorithm = DigestAlgorithm.RipeMD160; + return true; + } + + if (id == CmsSignedGenerator.DigestMD5) { + algorithm = DigestAlgorithm.MD5; + return true; + } + + if (id == PkcsObjectIdentifiers.MD4.Id) { + algorithm = DigestAlgorithm.MD4; + return true; + } + + if (id == PkcsObjectIdentifiers.MD2.Id) { + algorithm = DigestAlgorithm.MD2; + return true; + } + + algorithm = DigestAlgorithm.None; + + return false; + } + + //class VoteComparer : IComparer + //{ + // public int Compare (int x, int y) + // { + // return y - x; + // } + //} + + /// + /// Get the preferred encryption algorithm to use for encrypting to the specified recipients. + /// + /// + /// Gets the preferred encryption algorithm to use for encrypting to the specified recipients + /// based on the encryption algorithms supported by each of the recipients, the + /// , and the + /// . + /// If the supported encryption algorithms are unknown for any recipient, it is assumed that + /// the recipient supports at least the Triple-DES encryption algorithm. + /// + /// The preferred encryption algorithm. + /// The recipients. + protected virtual EncryptionAlgorithm GetPreferredEncryptionAlgorithm (CmsRecipientCollection recipients) + { + var votes = new int[EncryptionAlgorithmCount]; + int need = recipients.Count; + + foreach (var recipient in recipients) { + int cast = EncryptionAlgorithmCount; + + foreach (var algorithm in recipient.EncryptionAlgorithms) + votes[(int) algorithm]++; + } + + // Starting with S/MIME v3 (published in 1999), Triple-DES is a REQUIRED algorithm. + // S/MIME v2.x and older only required RC2/40, but SUGGESTED Triple-DES. + // Considering the fact that Bruce Schneier was able to write a + // screensaver that could crack RC2/40 back in the late 90's, let's + // not default to anything weaker than Triple-DES... + EncryptionAlgorithm chosen = EncryptionAlgorithm.TripleDes; + int nvotes = 0; + + votes[(int) EncryptionAlgorithm.TripleDes] = need; + + // iterate through the algorithms, from strongest to weakest, keeping track + // of the algorithm with the most amount of votes (between algorithms with + // the same number of votes, choose the strongest of the 2 - i.e. the one + // that we arrive at first). + var algorithms = EncryptionAlgorithmRank; + for (int i = 0; i < algorithms.Length; i++) { + var algorithm = algorithms[i]; + + if (!IsEnabled (algorithm)) + continue; + + if (votes[(int) algorithm] > nvotes) { + nvotes = votes[(int) algorithm]; + chosen = algorithm; + } + } + + return chosen; + } + + /// + /// Compresses the specified stream. + /// + /// + /// Compresses the specified stream. + /// + /// A new instance + /// containing the compressed content. + /// The stream to compress. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public ApplicationPkcs7Mime Compress (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var compresser = new CmsCompressedDataGenerator (); + var processable = new CmsProcessableInputStream (stream); + var compressed = compresser.Generate (processable, CmsCompressedDataGenerator.ZLib); + var encoded = compressed.GetEncoded (); + + return new ApplicationPkcs7Mime (SecureMimeType.CompressedData, new MemoryStream (encoded, false)); + } + + /// + /// Decompress the specified stream. + /// + /// + /// Decompress the specified stream. + /// + /// The decompressed mime part. + /// The stream to decompress. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public MimeEntity Decompress (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new CmsCompressedDataParser (stream); + var content = parser.GetContent (); + + return MimeEntity.Load (content.ContentStream); + } + + /// + /// Decompress the specified stream to an output stream. + /// + /// + /// Decompress the specified stream to an output stream. + /// + /// The stream to decompress. + /// The output stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public virtual void DecompressTo (Stream stream, Stream output) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + var parser = new CmsCompressedDataParser (stream); + var content = parser.GetContent (); + + content.ContentStream.CopyTo (output, 4096); + } + + internal SmimeCapabilitiesAttribute GetSecureMimeCapabilitiesAttribute (bool includeRsaesOaep) + { + var capabilities = new SmimeCapabilityVector (); + + foreach (var algorithm in EncryptionAlgorithmRank) { + if (!IsEnabled (algorithm)) + continue; + + switch (algorithm) { + case EncryptionAlgorithm.Aes128: + capabilities.AddCapability (SmimeCapabilities.Aes128Cbc); + break; + case EncryptionAlgorithm.Aes192: + capabilities.AddCapability (SmimeCapabilities.Aes192Cbc); + break; + case EncryptionAlgorithm.Aes256: + capabilities.AddCapability (SmimeCapabilities.Aes256Cbc); + break; + case EncryptionAlgorithm.Blowfish: + capabilities.AddCapability (Blowfish); + break; + case EncryptionAlgorithm.Camellia128: + capabilities.AddCapability (NttObjectIdentifiers.IdCamellia128Cbc); + break; + case EncryptionAlgorithm.Camellia192: + capabilities.AddCapability (NttObjectIdentifiers.IdCamellia192Cbc); + break; + case EncryptionAlgorithm.Camellia256: + capabilities.AddCapability (NttObjectIdentifiers.IdCamellia256Cbc); + break; + case EncryptionAlgorithm.Cast5: + capabilities.AddCapability (SmimeCapabilities.Cast5Cbc); + break; + case EncryptionAlgorithm.Des: + capabilities.AddCapability (SmimeCapabilities.DesCbc); + break; + case EncryptionAlgorithm.Idea: + capabilities.AddCapability (SmimeCapabilities.IdeaCbc); + break; + case EncryptionAlgorithm.RC240: + capabilities.AddCapability (SmimeCapabilities.RC2Cbc, 40); + break; + case EncryptionAlgorithm.RC264: + capabilities.AddCapability (SmimeCapabilities.RC2Cbc, 64); + break; + case EncryptionAlgorithm.RC2128: + capabilities.AddCapability (SmimeCapabilities.RC2Cbc, 128); + break; + case EncryptionAlgorithm.Seed: + capabilities.AddCapability (KisaObjectIdentifiers.IdSeedCbc); + break; + case EncryptionAlgorithm.TripleDes: + capabilities.AddCapability (SmimeCapabilities.DesEde3Cbc); + break; + //case EncryptionAlgorithm.Twofish: + // capabilities.AddCapability (Twofish); + // break; + } + } + + if (includeRsaesOaep) { + capabilities.AddCapability (PkcsObjectIdentifiers.IdRsaesOaep, RsaEncryptionPadding.OaepSha1.GetRsaesOaepParameters ()); + capabilities.AddCapability (PkcsObjectIdentifiers.IdRsaesOaep, RsaEncryptionPadding.OaepSha256.GetRsaesOaepParameters ()); + capabilities.AddCapability (PkcsObjectIdentifiers.IdRsaesOaep, RsaEncryptionPadding.OaepSha384.GetRsaesOaepParameters ()); + capabilities.AddCapability (PkcsObjectIdentifiers.IdRsaesOaep, RsaEncryptionPadding.OaepSha512.GetRsaesOaepParameters ()); + } + + return new SmimeCapabilitiesAttribute (capabilities); + } + + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + public abstract ApplicationPkcs7Mime EncapsulatedSign (CmsSigner signer, Stream content); + + /// + /// Cryptographically signs and encapsulates the content using the specified signer and digest algorithm. + /// + /// + /// Cryptographically signs and encapsulates the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public abstract ApplicationPkcs7Mime EncapsulatedSign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content); + + /// + /// Cryptographically signs the content using the specified signer. + /// + /// + /// Cryptographically signs the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + public abstract ApplicationPkcs7Signature Sign (CmsSigner signer, Stream content); + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The list of digital signatures. + /// The signed data. + /// The extracted MIME entity. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The extracted content could not be parsed as a MIME entity. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public abstract DigitalSignatureCollection Verify (Stream signedData, out MimeEntity entity, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The extracted content stream. + /// The signed data. + /// The digital signatures. + /// The cancellation token. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public abstract Stream Verify (Stream signedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Encrypts the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted content. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + public abstract ApplicationPkcs7Mime Encrypt (CmsRecipientCollection recipients, Stream content); + + /// + /// Decrypts the specified encryptedData to an output stream. + /// + /// + /// Decrypts the specified encryptedData to an output stream. + /// + /// The encrypted data. + /// The stream to write the decrypted data to. + /// + /// is null. + /// -or- + /// is null. + /// + public abstract void DecryptTo (Stream encryptedData, Stream decryptedData); + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// The raw certificate and key data. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// Importing keys is not supported by this cryptography context. + /// + public abstract void Import (Stream stream, string password); + + /// + /// Imports certificates and keys from a pkcs12 file. + /// + /// + /// Imports certificates and keys from a pkcs12 file. + /// + /// The raw certificate and key data in pkcs12 format. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// -or- + /// does not contain a private key. + /// -or- + /// does not contain a certificate that could be used for signing. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An I/O error occurred. + /// + /// + /// Importing keys is not supported by this cryptography context. + /// + public virtual void Import (string fileName, string password) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + using (var stream = File.OpenRead (fileName)) + Import (stream, password); + } + + /// + /// Imports the specified certificate. + /// + /// + /// Imports the specified certificate. + /// + /// The certificate. + /// + /// is null. + /// + public abstract void Import (X509Certificate certificate); + + /// + /// Imports the specified certificate revocation list. + /// + /// + /// Imports the specified certificate revocation list. + /// + /// The certificate revocation list. + /// + /// is null. + /// + public abstract void Import (X509Crl crl); + + /// + /// Imports certificates (as from a certs-only application/pkcs-mime part) + /// from the specified stream. + /// + /// + /// Imports certificates (as from a certs-only application/pkcs-mime part) + /// from the specified stream. + /// + /// The raw key data. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override void Import (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new CmsSignedDataParser (stream); + var certificates = parser.GetCertificates ("Collection"); + + foreach (X509Certificate certificate in certificates.GetMatches (null)) + Import (certificate); + + var crls = parser.GetCrls ("Collection"); + + foreach (X509Crl crl in crls.GetMatches (null)) + Import (crl); + } + } +} diff --git a/src/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs b/src/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs new file mode 100644 index 0000000..073460a --- /dev/null +++ b/src/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs @@ -0,0 +1,149 @@ +// +// SecureMimeDigitalCertificate.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using Org.BouncyCastle.X509; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME digital certificate. + /// + /// + /// An S/MIME digital certificate. + /// + public class SecureMimeDigitalCertificate : IDigitalCertificate + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// An X.509 certificate. + /// + /// is null. + /// + public SecureMimeDigitalCertificate (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + Certificate = certificate; + Fingerprint = certificate.GetFingerprint (); + PublicKeyAlgorithm = certificate.GetPublicKeyAlgorithm (); + } + + /// + /// Get the X.509 certificate. + /// + /// + /// Gets the X.509 certificate. + /// + /// The certificate. + public X509Certificate Certificate { + get; private set; + } + +// /// +// /// Gets the chain status. +// /// +// /// The chain status. +// public X509ChainStatusFlags ChainStatus { +// get; internal set; +// } + + #region IDigitalCertificate implementation + + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get; private set; + } + + /// + /// Gets the date that the certificate was created. + /// + /// + /// Gets the date that the certificate was created. + /// + /// The creation date. + public DateTime CreationDate { + get { return Certificate.NotBefore.ToUniversalTime (); } + } + + /// + /// Gets the expiration date of the certificate. + /// + /// + /// Gets the expiration date of the certificate. + /// + /// The expiration date. + public DateTime ExpirationDate { + get { return Certificate.NotAfter.ToUniversalTime (); } + } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// Gets the fingerprint of the certificate. + /// + /// The fingerprint. + public string Fingerprint { + get; private set; + } + + /// + /// Gets the email address of the owner of the certificate. + /// + /// + /// Gets the email address of the owner of the certificate. + /// + /// The email address. + public string Email { + get { return Certificate.GetSubjectEmailAddress (); } + } + + /// + /// Gets the name of the owner of the certificate. + /// + /// + /// Gets the name of the owner of the certificate. + /// + /// The name of the owner. + public string Name { + get { return Certificate.GetCommonName (); } + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/SecureMimeDigitalSignature.cs b/src/MimeKit/Cryptography/SecureMimeDigitalSignature.cs new file mode 100644 index 0000000..61c3f8c --- /dev/null +++ b/src/MimeKit/Cryptography/SecureMimeDigitalSignature.cs @@ -0,0 +1,265 @@ +// +// SecureMimeDigitalSignature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Cms; +using Org.BouncyCastle.Asn1.Smime; +using Org.BouncyCastle.Asn1.X509; + +using MimeKit.Utils; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME digital signature. + /// + /// + /// An S/MIME digital signature. + /// + public class SecureMimeDigitalSignature : IDigitalSignature + { + DigitalSignatureVerifyException vex; + bool? valid; + + static DateTime ToAdjustedDateTime (DerUtcTime time) + { + //try { + // return time.ToAdjustedDateTime (); + //} catch { + return DateUtils.Parse (time.AdjustedTimeString, "yyyyMMddHHmmsszzz"); + //} + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The information about the signer. + /// The signer's certificate. + /// + /// is null. + /// + public SecureMimeDigitalSignature (SignerInformation signerInfo, X509Certificate certificate) + { + if (signerInfo == null) + throw new ArgumentNullException (nameof (signerInfo)); + + SignerInfo = signerInfo; + + var algorithms = new List (); + DigestAlgorithm digestAlgo; + + if (signerInfo.SignedAttributes != null) { + Asn1EncodableVector vector = signerInfo.SignedAttributes.GetAll (CmsAttributes.SigningTime); + foreach (Org.BouncyCastle.Asn1.Cms.Attribute attr in vector) { + var signingTime = (DerUtcTime) ((DerSet) attr.AttrValues)[0]; + CreationDate = ToAdjustedDateTime (signingTime); + break; + } + + vector = signerInfo.SignedAttributes.GetAll (SmimeAttributes.SmimeCapabilities); + foreach (Org.BouncyCastle.Asn1.Cms.Attribute attr in vector) { + foreach (Asn1Sequence sequence in attr.AttrValues) { + for (int i = 0; i < sequence.Count; i++) { + var identifier = AlgorithmIdentifier.GetInstance (sequence[i]); + EncryptionAlgorithm algorithm; + + if (BouncyCastleSecureMimeContext.TryGetEncryptionAlgorithm (identifier, out algorithm)) + algorithms.Add (algorithm); + } + } + } + } + + EncryptionAlgorithms = algorithms.ToArray (); + + if (BouncyCastleSecureMimeContext.TryGetDigestAlgorithm (signerInfo.DigestAlgorithmID, out digestAlgo)) + DigestAlgorithm = digestAlgo; + + if (certificate != null) + SignerCertificate = new SecureMimeDigitalCertificate (certificate); + } + + /// + /// Gets the signer info. + /// + /// + /// Gets the signer info. + /// + /// The signer info. + public SignerInformation SignerInfo { + get; private set; + } + + /// + /// Gets the list of encryption algorithms, in preferential order, + /// that the signer's client supports. + /// + /// + /// Gets the list of encryption algorithms, in preferential order, + /// that the signer's client supports. + /// + /// The S/MIME encryption algorithms. + public EncryptionAlgorithm[] EncryptionAlgorithms { + get; private set; + } + + /// + /// Gets the certificate chain. + /// + /// + /// If building the certificate chain failed, this value will be null and + /// will be set. + /// + /// The certificate chain. + public PkixCertPath Chain { + get; internal set; + } + + /// + /// The exception that occurred, if any, while building the certificate chain. + /// + /// + /// This will only be set if building the certificate chain failed. + /// + /// The exception. + public Exception ChainException { + get; internal set; + } + + #region IDigitalSignature implementation + + /// + /// Gets certificate used by the signer. + /// + /// + /// Gets certificate used by the signer. + /// + /// The signer's certificate. + public IDigitalCertificate SignerCertificate { + get; private set; + } + + /// + /// Gets the public key algorithm used for the signature. + /// + /// + /// Gets the public key algorithm used for the signature. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get { return SignerCertificate != null ? SignerCertificate.PublicKeyAlgorithm : PublicKeyAlgorithm.None; } + } + + /// + /// Gets the digest algorithm used for the signature. + /// + /// + /// Gets the digest algorithm used for the signature. + /// + /// The digest algorithm. + public DigestAlgorithm DigestAlgorithm { + get; private set; + } + + /// + /// Gets the creation date of the digital signature. + /// + /// + /// Gets the creation date of the digital signature. + /// + /// The creation date in coordinated universal time (UTC). + public DateTime CreationDate { + get; private set; + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify () + { + return Verify (false); + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if only the signature itself should be verified; otherwise, both the signature and the certificate chain are validated. + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify (bool verifySignatureOnly) + { + if (vex != null) + throw vex; + + if (SignerCertificate == null) { + var message = string.Format ("Failed to verify digital signature: missing certificate."); + vex = new DigitalSignatureVerifyException (message); + throw vex; + } + + if (!valid.HasValue) { + try { + var certificate = ((SecureMimeDigitalCertificate) SignerCertificate).Certificate; + valid = SignerInfo.Verify (certificate); + } catch (Exception ex) { + var message = string.Format ("Failed to verify digital signature: {0}", ex.Message); + vex = new DigitalSignatureVerifyException (message, ex); + throw vex; + } + } + + if (!verifySignatureOnly && ChainException != null) { + var message = string.Format ("Failed to verify digital signature chain: {0}", ChainException.Message); + + throw new DigitalSignatureVerifyException (message, ChainException); + } + + return valid.Value; + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/SecureMimeType.cs b/src/MimeKit/Cryptography/SecureMimeType.cs new file mode 100644 index 0000000..150e002 --- /dev/null +++ b/src/MimeKit/Cryptography/SecureMimeType.cs @@ -0,0 +1,65 @@ +// +// SecureMimeType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// The type of S/MIME data that an application/pkcs7-mime part contains. + /// + /// + /// The type of S/MIME data that an application/pkcs7-mime part contains. + /// + public enum SecureMimeType { + /// + /// The S/MIME data type is unknown. + /// + Unknown = -1, + + /// + /// The S/MIME content is compressed. + /// + CompressedData, + + /// + /// The S/MIME content is encrypted. + /// + EnvelopedData, + + /// + /// The S/MIME content is signed. + /// + SignedData, + + /// + /// The S/MIME content contains only certificates. + /// + CertsOnly, + + /// + /// The S/MIME content is both signed and encrypted. + /// + AuthEnvelopedData, + } +} diff --git a/src/MimeKit/Cryptography/SqlCertificateDatabase.cs b/src/MimeKit/Cryptography/SqlCertificateDatabase.cs new file mode 100644 index 0000000..4bdaada --- /dev/null +++ b/src/MimeKit/Cryptography/SqlCertificateDatabase.cs @@ -0,0 +1,832 @@ +// +// SqlCertificateDatabase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Data; +using System.Text; +using System.Data.Common; +using System.Collections.Generic; + +#if __MOBILE__ +using Mono.Data.Sqlite; +#else +using System.Reflection; +#endif + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// An abstract X.509 certificate database built on generic SQL storage. + /// + /// + /// An X.509 certificate database is used for storing certificates, metdata related to the certificates + /// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs), + /// and private keys. + /// This particular database uses SQLite to store the data. + /// + public abstract class SqlCertificateDatabase : X509CertificateDatabase + { + readonly DataTable certificatesTable, crlsTable; + readonly DbConnection connection; + bool disposed; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new using the provided database connection. + /// + /// The database . + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + protected SqlCertificateDatabase (DbConnection connection, string password) : base (password) + { + if (connection == null) + throw new ArgumentNullException (nameof (connection)); + + this.connection = connection; + + if (connection.State != ConnectionState.Open) + connection.Open (); + + certificatesTable = CreateCertificatesDataTable ("CERTIFICATES"); + crlsTable = CreateCrlsDataTable ("CRLS"); + + CreateCertificatesTable (certificatesTable); + CreateCrlsTable (crlsTable); + } + +#if NETSTANDARD1_3 || NETSTANDARD1_6 +#pragma warning disable 1591 + protected class DataColumn + { + public DataColumn (string columnName, Type dataType) + { + ColumnName = columnName; + DataType = dataType; + } + + public DataColumn () + { + } + + public bool AllowDBNull { + get; set; + } + + public bool AutoIncrement { + get; set; + } + + public string ColumnName { + get; set; + } + + public Type DataType { + get; set; + } + + public bool Unique { + get; set; + } + } + + protected class DataColumnCollection : List + { + public int IndexOf (string columnName) + { + for (int i = 0; i < Count; i++) { + if (this[i].ColumnName.Equals (columnName, StringComparison.Ordinal)) + return i; + } + + return -1; + } + } + + protected class DataTable + { + public DataTable (string tableName) + { + Columns = new DataColumnCollection (); + TableName = tableName; + } + + public string TableName { + get; set; + } + + public DataColumnCollection Columns { + get; private set; + } + + public DataColumn[] PrimaryKey { + get; set; + } + } +#pragma warning restore 1591 +#endif + + static DataTable CreateCertificatesDataTable (string tableName) + { + var table = new DataTable (tableName); + table.Columns.Add (new DataColumn ("ID", typeof (int)) { AutoIncrement = true }); + table.Columns.Add (new DataColumn ("TRUSTED", typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("ANCHOR", typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("BASICCONSTRAINTS", typeof (int)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("KEYUSAGE", typeof (int)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("NOTBEFORE", typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("NOTAFTER", typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("ISSUERNAME", typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("SERIALNUMBER", typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("SUBJECTNAME", typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("SUBJECTKEYIDENTIFIER", typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn ("SUBJECTEMAIL", typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn ("FINGERPRINT", typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("ALGORITHMS", typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn ("ALGORITHMSUPDATED", typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("CERTIFICATE", typeof (byte[])) { AllowDBNull = false, Unique = true }); + table.Columns.Add (new DataColumn ("PRIVATEKEY", typeof (byte[])) { AllowDBNull = true }); + table.PrimaryKey = new DataColumn[] { table.Columns[0] }; + + return table; + } + + static DataTable CreateCrlsDataTable (string tableName) + { + var table = new DataTable (tableName); + table.Columns.Add (new DataColumn ("ID", typeof (int)) { AutoIncrement = true }); + table.Columns.Add (new DataColumn ("DELTA", typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("ISSUERNAME", typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("THISUPDATE", typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("NEXTUPDATE", typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn ("CRL", typeof (byte[])) { AllowDBNull = false }); + table.PrimaryKey = new DataColumn[] { table.Columns[0] }; + + return table; + } + + /// + /// Gets the columns for the specified table. + /// + /// + /// Gets the list of columns for the specified table. + /// + /// The . + /// The name of the table. + /// The list of columns. + protected abstract IList GetTableColumns (DbConnection connection, string tableName); + + /// + /// Gets the command to create a table. + /// + /// + /// Constructs the command to create a table. + /// + /// The . + /// The table. + protected abstract void CreateTable (DbConnection connection, DataTable table); + + /// + /// Adds a column to a table. + /// + /// + /// Adds a column to a table. + /// + /// The . + /// The table. + /// The column to add. + protected abstract void AddTableColumn (DbConnection connection, DataTable table, DataColumn column); + + static string GetIndexName (string tableName, string[] columnNames) + { + return string.Format ("{0}_{1}_INDEX", tableName, string.Join ("_", columnNames)); + } + + static void CreateIndex (DbConnection connection, string tableName, string[] columnNames) + { + var indexName = GetIndexName (tableName, columnNames); + var query = string.Format ("CREATE INDEX IF NOT EXISTS {0} ON {1}({2})", indexName, tableName, string.Join (", ", columnNames)); + + using (var command = connection.CreateCommand ()) { + command.CommandText = query; + command.ExecuteNonQuery (); + } + } + + static void RemoveIndex (DbConnection connection, string tableName, string[] columnNames) + { + var indexName = GetIndexName (tableName, columnNames); + var query = string.Format ("DROP INDEX IF EXISTS {0}", indexName); + + using (var command = connection.CreateCommand ()) { + command.CommandText = query; + command.ExecuteNonQuery (); + } + } + + void CreateCertificatesTable (DataTable table) + { + CreateTable (connection, table); + + var currentColumns = GetTableColumns (connection, table.TableName); + bool hasAnchorColumn = false; + + for (int i = 0; i < currentColumns.Count; i++) { + if (currentColumns[i].ColumnName.Equals ("ANCHOR", StringComparison.Ordinal)) { + hasAnchorColumn = true; + break; + } + } + + // Note: The ANCHOR, SUBJECTNAME and SUBJECTKEYIDENTIFIER columns were all added in the same version, + // so if the ANCHOR column is missing, they all are. + if (!hasAnchorColumn) { + using (var transaction = connection.BeginTransaction ()) { + try { + var column = table.Columns[table.Columns.IndexOf ("ANCHOR")]; + AddTableColumn (connection, table, column); + + column = table.Columns[table.Columns.IndexOf ("SUBJECTNAME")]; + AddTableColumn (connection, table, column); + + column = table.Columns[table.Columns.IndexOf ("SUBJECTKEYIDENTIFIER")]; + AddTableColumn (connection, table, column); + + foreach (var record in Find (null, false, X509CertificateRecordFields.Id | X509CertificateRecordFields.Certificate)) { + var statement = "UPDATE CERTIFICATES SET ANCHOR = @ANCHOR, SUBJECTNAME = @SUBJECTNAME, SUBJECTKEYIDENTIFIER = @SUBJECTKEYIDENTIFIER WHERE ID = @ID"; + var command = connection.CreateCommand (); + + command.AddParameterWithValue ("@ID", record.Id); + command.AddParameterWithValue ("@ANCHOR", record.IsAnchor); + command.AddParameterWithValue ("@SUBJECTNAME", record.SubjectName); + command.AddParameterWithValue ("@SUBJECTKEYIDENTIFIER", record.SubjectKeyIdentifier?.AsHex ()); + command.CommandType = CommandType.Text; + command.CommandText = statement; + + command.ExecuteNonQuery (); + } + + transaction.Commit (); + } catch { + transaction.Rollback (); + throw; + } + } + + // Remove some old indexes + RemoveIndex (connection, table.TableName, new[] { "TRUSTED" }); + RemoveIndex (connection, table.TableName, new[] { "TRUSTED", "BASICCONSTRAINTS", "ISSUERNAME", "SERIALNUMBER" }); + RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "ISSUERNAME", "SERIALNUMBER" }); + RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "FINGERPRINT" }); + RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "SUBJECTEMAIL" }); + } + + // Note: Use "EXPLAIN QUERY PLAN SELECT ... FROM CERTIFICATES WHERE ..." to verify that any indexes we create get used as expected. + + // Index for matching against a specific certificate + CreateIndex (connection, table.TableName, new [] { "ISSUERNAME", "SERIALNUMBER", "FINGERPRINT" }); + + // Index for searching for a certificate based on a SecureMailboxAddress + CreateIndex (connection, table.TableName, new [] { "BASICCONSTRAINTS", "FINGERPRINT", "NOTBEFORE", "NOTAFTER" }); + + // Index for searching for a certificate based on a MailboxAddress + CreateIndex (connection, table.TableName, new [] { "BASICCONSTRAINTS", "SUBJECTEMAIL", "NOTBEFORE", "NOTAFTER" }); + + // Index for gathering a list of Trusted Anchors + CreateIndex (connection, table.TableName, new [] { "TRUSTED", "ANCHOR", "KEYUSAGE" }); + } + + void CreateCrlsTable (DataTable table) + { + CreateTable (connection, table); + + CreateIndex (connection, table.TableName, new [] { "ISSUERNAME" }); + CreateIndex (connection, table.TableName, new [] { "DELTA", "ISSUERNAME", "THISUPDATE" }); + } + + static StringBuilder CreateSelectQuery (X509CertificateRecordFields fields) + { + var query = new StringBuilder ("SELECT "); + var columns = GetColumnNames (fields); + + for (int i = 0; i < columns.Length; i++) { + if (i > 0) + query = query.Append (", "); + + query = query.Append (columns[i]); + } + + return query.Append (" FROM CERTIFICATES"); + } + + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// The database command. + /// The certificate. + /// The fields to return. + protected override DbCommand GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields) + { + var fingerprint = certificate.GetFingerprint ().ToLowerInvariant (); + var serialNumber = certificate.SerialNumber.ToString (); + var issuerName = certificate.IssuerDN.ToString (); + var command = connection.CreateCommand (); + var query = CreateSelectQuery (fields); + + // FIXME: Is this really the best way to query for an exact match of a certificate? + query = query.Append (" WHERE ISSUERNAME = @ISSUERNAME AND SERIALNUMBER = @SERIALNUMBER AND FINGERPRINT = @FINGERPRINT LIMIT 1"); + command.AddParameterWithValue ("@ISSUERNAME", issuerName); + command.AddParameterWithValue ("@SERIALNUMBER", serialNumber); + command.AddParameterWithValue ("@FINGERPRINT", fingerprint); + + command.CommandText = query.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Get the database command to select the certificate records for the specified mailbox. + /// + /// + /// Gets the database command to select the certificate records for the specified mailbox. + /// + /// The database command. + /// The mailbox. + /// The date and time for which the certificate should be valid. + /// true + /// The fields to return. + protected override DbCommand GetSelectCommand (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields) + { + var secure = mailbox as SecureMailboxAddress; + var command = connection.CreateCommand (); + var query = CreateSelectQuery (fields); + + query = query.Append (" WHERE BASICCONSTRAINTS = @BASICCONSTRAINTS "); + command.AddParameterWithValue ("@BASICCONSTRAINTS", -1); + + if (secure != null && !string.IsNullOrEmpty (secure.Fingerprint)) { + if (secure.Fingerprint.Length < 40) { + command.AddParameterWithValue ("@FINGERPRINT", secure.Fingerprint.ToLowerInvariant () + "%"); + query = query.Append ("AND FINGERPRINT LIKE @FINGERPRINT "); + } else { + command.AddParameterWithValue ("@FINGERPRINT", secure.Fingerprint.ToLowerInvariant ()); + query = query.Append ("AND FINGERPRINT = @FINGERPRINT "); + } + } else { + command.AddParameterWithValue ("@SUBJECTEMAIL", mailbox.Address.ToLowerInvariant ()); + query = query.Append ("AND SUBJECTEMAIL = @SUBJECTEMAIL "); + } + + query = query.Append ("AND NOTBEFORE < @NOW AND NOTAFTER > @NOW"); + command.AddParameterWithValue ("@NOW", now.ToUniversalTime ()); + + if (requirePrivateKey) + query = query.Append (" AND PRIVATEKEY IS NOT NULL"); + + command.CommandText = query.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Get the database command to select the requested certificate records. + /// + /// + /// Gets the database command to select the requested certificate records. + /// + /// The database command. + /// The certificate selector. + /// true if only trusted anchor certificates should be matched; otherwise, false. + /// true if the certificate must have a private key; otherwise, false. + /// The fields to return. + protected override DbCommand GetSelectCommand (IX509Selector selector, bool trustedAnchorsOnly, bool requirePrivateKey, X509CertificateRecordFields fields) + { + var match = selector as X509CertStoreSelector; + var command = connection.CreateCommand (); + var query = CreateSelectQuery (fields); + int baseQueryLength = query.Length; + + query = query.Append (" WHERE "); + + // FIXME: We could create an X509CertificateDatabaseSelector subclass of X509CertStoreSelector that + // adds properties like bool Trusted, bool Anchor, and bool HasPrivateKey ? Then we could drop the + // bool method arguments... + if (trustedAnchorsOnly) { + query = query.Append ("TRUSTED = @TRUSTED AND ANCHOR = @ANCHOR"); + command.AddParameterWithValue ("@TRUSTED", true); + command.AddParameterWithValue ("@ANCHOR", true); + } + + if (match != null) { + if (match.BasicConstraints >= 0 || match.BasicConstraints == -2) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + if (match.BasicConstraints == -2) { + command.AddParameterWithValue ("@BASICCONSTRAINTS", -1); + query = query.Append ("BASICCONSTRAINTS = @BASICCONSTRAINTS"); + } else { + command.AddParameterWithValue ("@BASICCONSTRAINTS", match.BasicConstraints); + query = query.Append ("BASICCONSTRAINTS >= @BASICCONSTRAINTS"); + } + } + + if (match.CertificateValid != null) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@DATETIME", match.CertificateValid.Value.ToUniversalTime ()); + query = query.Append ("NOTBEFORE < @DATETIME AND NOTAFTER > @DATETIME"); + } + + if (match.Issuer != null || match.Certificate != null) { + // Note: GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields) + // queries for ISSUERNAME, SERIALNUMBER, and FINGERPRINT so we'll do the same. + var issuer = match.Issuer ?? match.Certificate.IssuerDN; + + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@ISSUERNAME", issuer.ToString ()); + query = query.Append ("ISSUERNAME = @ISSUERNAME"); + } + + if (match.SerialNumber != null || match.Certificate != null) { + // Note: GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields) + // queries for ISSUERNAME, SERIALNUMBER, and FINGERPRINT so we'll do the same. + var serialNumber = match.SerialNumber ?? match.Certificate.SerialNumber; + + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@SERIALNUMBER", serialNumber.ToString ()); + query = query.Append ("SERIALNUMBER = @SERIALNUMBER"); + } + + if (match.Certificate != null) { + // Note: GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields) + // queries for ISSUERNAME, SERIALNUMBER, and FINGERPRINT so we'll do the same. + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@FINGERPRINT", match.Certificate.GetFingerprint ()); + query = query.Append ("FINGERPRINT = @FINGERPRINT"); + } + + if (match.Subject != null) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@SUBJECTNAME", match.Subject.ToString ()); + query = query.Append ("SUBJECTNAME = @SUBJECTNAME"); + } + + if (match.SubjectKeyIdentifier != null) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + var id = (Asn1OctetString) Asn1Object.FromByteArray (match.SubjectKeyIdentifier); + var subjectKeyIdentifier = id.GetOctets ().AsHex (); + + command.AddParameterWithValue ("@SUBJECTKEYIDENTIFIER", subjectKeyIdentifier); + query = query.Append ("SUBJECTKEYIDENTIFIER = @SUBJECTKEYIDENTIFIER"); + } + + if (match.KeyUsage != null) { + var flags = BouncyCastleCertificateExtensions.GetKeyUsageFlags (match.KeyUsage); + + if (flags != X509KeyUsageFlags.None) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + command.AddParameterWithValue ("@FLAGS", (int) flags); + query = query.Append ("(KEYUSAGE = 0 OR (KEYUSAGE & @FLAGS) = @FLAGS)"); + } + } + } + + if (requirePrivateKey) { + if (command.Parameters.Count > 0) + query = query.Append (" AND "); + + query = query.Append ("PRIVATEKEY IS NOT NULL"); + } else if (command.Parameters.Count == 0) { + query.Length = baseQueryLength; + } + + command.CommandText = query.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to select the CRL records matching the specified issuer. + /// + /// + /// Gets the database command to select the CRL records matching the specified issuer. + /// + /// The database command. + /// The issuer. + /// The fields to return. + protected override DbCommand GetSelectCommand (X509Name issuer, X509CrlRecordFields fields) + { + var query = "SELECT " + string.Join (", ", GetColumnNames (fields)) + " FROM CRLS "; + var command = connection.CreateCommand (); + + command.CommandText = query + "WHERE ISSUERNAME = @ISSUERNAME"; + command.AddParameterWithValue ("@ISSUERNAME", issuer.ToString ()); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to select the record for the specified CRL. + /// + /// + /// Gets the database command to select the record for the specified CRL. + /// + /// The database command. + /// The X.509 CRL. + /// The fields to return. + protected override DbCommand GetSelectCommand (X509Crl crl, X509CrlRecordFields fields) + { + var query = "SELECT " + string.Join (", ", GetColumnNames (fields)) + " FROM CRLS "; + var issuerName = crl.IssuerDN.ToString (); + var command = connection.CreateCommand (); + + command.CommandText = query + "WHERE DELTA = @DELTA AND ISSUERNAME = @ISSUERNAME AND THISUPDATE = @THISUPDATE LIMIT 1"; + command.AddParameterWithValue ("@DELTA", crl.IsDelta ()); + command.AddParameterWithValue ("@ISSUERNAME", issuerName); + command.AddParameterWithValue ("@THISUPDATE", crl.ThisUpdate.ToUniversalTime ()); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to select all CRLs in the table. + /// + /// + /// Gets the database command to select all CRLs in the table. + /// + /// The database command. + protected override DbCommand GetSelectAllCrlsCommand () + { + var command = connection.CreateCommand (); + + command.CommandText = "SELECT ID, CRL FROM CRLS"; + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to delete the specified certificate record. + /// + /// + /// Gets the database command to delete the specified certificate record. + /// + /// The database command. + /// The certificate record. + protected override DbCommand GetDeleteCommand (X509CertificateRecord record) + { + var command = connection.CreateCommand (); + + command.CommandText = "DELETE FROM CERTIFICATES WHERE ID = @ID"; + command.AddParameterWithValue ("@ID", record.Id); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to delete the specified CRL record. + /// + /// + /// Gets the database command to delete the specified CRL record. + /// + /// The database command. + /// The record. + protected override DbCommand GetDeleteCommand (X509CrlRecord record) + { + var command = connection.CreateCommand (); + + command.CommandText = "DELETE FROM CRLS WHERE ID = @ID"; + command.AddParameterWithValue ("@ID", record.Id); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to insert the specified certificate record. + /// + /// + /// Gets the database command to insert the specified certificate record. + /// + /// The database command. + /// The certificate record. + protected override DbCommand GetInsertCommand (X509CertificateRecord record) + { + var statement = new StringBuilder ("INSERT INTO CERTIFICATES("); + var variables = new StringBuilder ("VALUES("); + var command = connection.CreateCommand (); + var columns = certificatesTable.Columns; + + for (int i = 1; i < columns.Count; i++) { + if (i > 1) { + statement.Append (", "); + variables.Append (", "); + } + + var value = GetValue (record, columns[i].ColumnName); + var variable = "@" + columns[i]; + + command.AddParameterWithValue (variable, value); + statement.Append (columns[i]); + variables.Append (variable); + } + + statement.Append (')'); + variables.Append (')'); + + command.CommandText = statement + " " + variables; + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to insert the specified CRL record. + /// + /// + /// Gets the database command to insert the specified CRL record. + /// + /// The database command. + /// The CRL record. + protected override DbCommand GetInsertCommand (X509CrlRecord record) + { + var statement = new StringBuilder ("INSERT INTO CRLS("); + var variables = new StringBuilder ("VALUES("); + var command = connection.CreateCommand (); + var columns = crlsTable.Columns; + + for (int i = 1; i < columns.Count; i++) { + if (i > 1) { + statement.Append (", "); + variables.Append (", "); + } + + var value = GetValue (record, columns[i].ColumnName); + var variable = "@" + columns[i]; + + command.AddParameterWithValue (variable, value); + statement.Append (columns[i]); + variables.Append (variable); + } + + statement.Append (')'); + variables.Append (')'); + + command.CommandText = statement + " " + variables; + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to update the specified record. + /// + /// + /// Gets the database command to update the specified record. + /// + /// The database command. + /// The certificate record. + /// The fields to update. + protected override DbCommand GetUpdateCommand (X509CertificateRecord record, X509CertificateRecordFields fields) + { + var statement = new StringBuilder ("UPDATE CERTIFICATES SET "); + var columns = GetColumnNames (fields & ~X509CertificateRecordFields.Id); + var command = connection.CreateCommand (); + + for (int i = 0; i < columns.Length; i++) { + var value = GetValue (record, columns[i]); + var variable = "@" + columns[i]; + + if (i > 0) + statement.Append (", "); + + statement.Append (columns[i]); + statement.Append (" = "); + statement.Append (variable); + + command.AddParameterWithValue (variable, value); + } + + statement.Append (" WHERE ID = @ID"); + command.AddParameterWithValue ("@ID", record.Id); + + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Gets the database command to update the specified CRL record. + /// + /// + /// Gets the database command to update the specified CRL record. + /// + /// The database command. + /// The CRL record. + protected override DbCommand GetUpdateCommand (X509CrlRecord record) + { + var statement = new StringBuilder ("UPDATE CRLS SET "); + var command = connection.CreateCommand (); + var columns = crlsTable.Columns; + + for (int i = 1; i < columns.Count; i++) { + var value = GetValue (record, columns[i].ColumnName); + var variable = "@" + columns[i]; + + if (i > 1) + statement.Append (", "); + + statement.Append (columns[i]); + statement.Append (" = "); + statement.Append (variable); + + command.AddParameterWithValue (variable, value); + } + + statement.Append (" WHERE ID = @ID"); + command.AddParameterWithValue ("@ID", record.Id); + + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + + return command; + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + if (connection != null) + connection.Dispose (); + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/Cryptography/SqliteCertificateDatabase.cs b/src/MimeKit/Cryptography/SqliteCertificateDatabase.cs new file mode 100644 index 0000000..c37e68f --- /dev/null +++ b/src/MimeKit/Cryptography/SqliteCertificateDatabase.cs @@ -0,0 +1,394 @@ +// +// SqliteCertificateDatabase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Data; +using System.Text; +using System.Data.Common; +using System.Collections.Generic; + +#if __MOBILE__ +using Mono.Data.Sqlite; +#else +using System.Reflection; +#endif + +namespace MimeKit.Cryptography { + /// + /// An X.509 certificate database built on SQLite. + /// + /// + /// An X.509 certificate database is used for storing certificates, metdata related to the certificates + /// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs), + /// and private keys. + /// This particular database uses SQLite to store the data. + /// + public class SqliteCertificateDatabase : SqlCertificateDatabase + { +#if !__MOBILE__ + class SQLiteAssembly + { + public Type ConnectionStringBuilderType { get; private set; } + public Type ConnectionType { get; private set; } + public Assembly Assembly { get; private set; } + + public PropertyInfo ConnectionStringProperty { get; private set; } + public PropertyInfo DateTimeFormatProperty { get; private set; } + public PropertyInfo DataSourceProperty { get; private set; } + + public static SQLiteAssembly Load (string assemblyName) + { + try { + int dot = assemblyName.LastIndexOf ('.'); + var prefix = assemblyName.Substring (dot + 1); + + var assembly = Assembly.Load (new AssemblyName (assemblyName)); + var builderType = assembly.GetType (assemblyName + "." + prefix + "ConnectionStringBuilder"); + var connectionType = assembly.GetType (assemblyName + "." + prefix + "Connection"); + var connectionString = builderType.GetProperty ("ConnectionString"); + var dateTimeFormat = builderType.GetProperty ("DateTimeFormat"); + var dataSource = builderType.GetProperty ("DataSource"); + + return new SQLiteAssembly { + Assembly = assembly, + ConnectionType = connectionType, + ConnectionStringBuilderType = builderType, + ConnectionStringProperty = connectionString, + DateTimeFormatProperty = dateTimeFormat, + DataSourceProperty = dataSource + }; + } catch { + return null; + } + } + } + + static readonly SQLiteAssembly sqliteAssembly; +#endif + + // At class initialization we try to use reflection to load the + // Mono.Data.Sqlite assembly: this allows us to use Sqlite as the + // default certificate store without explicitly depending on the + // assembly. + static SqliteCertificateDatabase () + { +#if __MOBILE__ + IsAvailable = true; +#else // !__MOBILE__ +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP3_0 + var platform = Environment.OSVersion.Platform; +#endif + +#if NETSTANDARD1_3 || NETSTANDARD1_6 || NETSTANDARD2_0 || NETCOREAPP3_0 + if ((sqliteAssembly = SQLiteAssembly.Load ("Microsoft.Data.Sqlite")) != null) { + // Make sure that the runtime can load the native sqlite library + if (VerifySQLiteAssemblyIsUsable ()) { + IsAvailable = true; + return; + } + } +#endif + +#if NETFRAMEWORK || NETCOREAPP3_0 + // Mono.Data.Sqlite will only work on Unix-based platforms. + if (platform == PlatformID.Unix || platform == PlatformID.MacOSX) { + if ((sqliteAssembly = SQLiteAssembly.Load ("Mono.Data.Sqlite")) != null) { + // Make sure that the runtime can load the native sqlite3 library + if (VerifySQLiteAssemblyIsUsable ()) { + IsAvailable = true; + return; + } + } + } +#endif + +#if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP3_0 + if ((sqliteAssembly = SQLiteAssembly.Load ("System.Data.SQLite")) != null) { + // Make sure that the runtime can load the native sqlite3 library + if (VerifySQLiteAssemblyIsUsable ()) { + IsAvailable = true; + return; + } + } +#endif +#endif // __MOBILE__ + } + +#if !__MOBILE__ + static bool VerifySQLiteAssemblyIsUsable () + { + // Make sure that the runtime can load the native sqlite3 library. + var fileName = Path.GetTempFileName (); + + try { + var connection = CreateConnection (fileName); + connection.Dispose (); + return true; + } catch { + return false; + } finally { + File.Delete (fileName); + } + } +#endif + + internal static bool IsAvailable { + get; private set; + } + + static DbConnection CreateConnection (string fileName) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (fileName.Length == 0) + throw new ArgumentException ("The file name cannot be empty.", nameof (fileName)); + + if (!File.Exists (fileName)) { + var dir = Path.GetDirectoryName (fileName); + + if (!string.IsNullOrEmpty (dir) && !Directory.Exists (dir)) + Directory.CreateDirectory (dir); + +#if __MOBILE__ + SqliteConnection.CreateFile (fileName); +#else + File.Create (fileName).Dispose (); +#endif + } + +#if !__MOBILE__ + var builder = Activator.CreateInstance (sqliteAssembly.ConnectionStringBuilderType); + + sqliteAssembly.DataSourceProperty.SetValue (builder, fileName, null); + sqliteAssembly.DateTimeFormatProperty?.SetValue (builder, 0, null); + + var connectionString = (string) sqliteAssembly.ConnectionStringProperty.GetValue (builder, null); + + return (DbConnection) Activator.CreateInstance (sqliteAssembly.ConnectionType, new [] { connectionString }); +#else + var builder = new SqliteConnectionStringBuilder (); + builder.DateTimeFormat = SQLiteDateFormats.Ticks; + builder.DataSource = fileName; + + return new SqliteConnection (builder.ConnectionString); +#endif + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new and opens a connection to the + /// SQLite database at the specified path using the Mono.Data.Sqlite binding to the native + /// SQLite library. + /// If Mono.Data.Sqlite is not available or if an alternative binding to the native + /// SQLite library is preferred, then consider using + /// instead. + /// + /// The file name. + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The specified file path is empty. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An error occurred reading the file. + /// + public SqliteCertificateDatabase (string fileName, string password) : this (CreateConnection (fileName), password) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new using the provided SQLite database connection. + /// + /// The SQLite connection. + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + public SqliteCertificateDatabase (DbConnection connection, string password) : base (connection, password) + { + } + + /// + /// Gets the columns for the specified table. + /// + /// + /// Gets the list of columns for the specified table. + /// + /// The . + /// The name of the table. + /// The list of columns. + protected override IList GetTableColumns (DbConnection connection, string tableName) + { + using (var command = connection.CreateCommand ()) { + command.CommandText = $"PRAGMA table_info({tableName})"; + using (var reader = command.ExecuteReader ()) { + var columns = new List (); + + while (reader.Read ()) { + var column = new DataColumn (); + + for (int i = 0; i < reader.FieldCount; i++) { + var field = reader.GetName (i).ToUpperInvariant (); + + switch (field) { + case "NAME": + column.ColumnName = reader.GetString (i); + break; + case "TYPE": + var type = reader.GetString (i); + switch (type) { + case "INTEGER": column.DataType = typeof (long); break; + case "BLOB": column.DataType = typeof (byte[]); break; + case "TEXT": column.DataType = typeof (string); break; + } + break; + case "NOTNULL": + column.AllowDBNull = !reader.GetBoolean (i); + break; + } + } + + columns.Add (column); + } + + return columns; + } + } + } + + static void Build (StringBuilder statement, DataTable table, DataColumn column, ref int primaryKeys, bool create) + { + statement.Append (column.ColumnName); + statement.Append (' '); + + if (column.DataType == typeof (long) || column.DataType == typeof (int) || column.DataType == typeof (bool)) { + statement.Append ("INTEGER"); + } else if (column.DataType == typeof (byte[])) { + statement.Append ("BLOB"); + } else if (column.DataType == typeof (string)) { + statement.Append ("TEXT"); + } else { + throw new NotImplementedException (); + } + + bool isPrimaryKey = false; + if (table != null && table.PrimaryKey != null && primaryKeys < table.PrimaryKey.Length) { + for (int i = 0; i < table.PrimaryKey.Length; i++) { + if (column == table.PrimaryKey[i]) { + statement.Append (" PRIMARY KEY"); + isPrimaryKey = true; + primaryKeys++; + break; + } + } + } + + if (column.AutoIncrement) + statement.Append (" AUTOINCREMENT"); + + if (column.Unique && !isPrimaryKey) + statement.Append (" UNIQUE"); + + // Note: Normally we'd want to include NOT NULL, but we can't *add* new columns with the NOT NULL restriction + if (create && !column.AllowDBNull) + statement.Append (" NOT NULL"); + } + + /// + /// Create a table. + /// + /// + /// Creates the specified table. + /// + /// The . + /// The table. + protected override void CreateTable (DbConnection connection, DataTable table) + { + var statement = new StringBuilder ("CREATE TABLE IF NOT EXISTS "); + int primaryKeys = 0; + + statement.Append (table.TableName); + statement.Append ('('); + + foreach (DataColumn column in table.Columns) { + Build (statement, table, column, ref primaryKeys, true); + statement.Append (", "); + } + + if (table.Columns.Count > 0) + statement.Length -= 2; + + statement.Append (')'); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + + /// + /// Adds a column to a table. + /// + /// + /// Adds a column to a table. + /// + /// The . + /// The table. + /// The column to add. + protected override void AddTableColumn (DbConnection connection, DataTable table, DataColumn column) + { + var statement = new StringBuilder ("ALTER TABLE "); + int primaryKeys = table.PrimaryKey?.Length ?? 0; + + statement.Append (table.TableName); + statement.Append (" ADD COLUMN "); + Build (statement, table, column, ref primaryKeys, false); + + using (var command = connection.CreateCommand ()) { + command.CommandText = statement.ToString (); + command.CommandType = CommandType.Text; + command.ExecuteNonQuery (); + } + } + } +} diff --git a/src/MimeKit/Cryptography/SubjectIdentifierType.cs b/src/MimeKit/Cryptography/SubjectIdentifierType.cs new file mode 100644 index 0000000..6206f45 --- /dev/null +++ b/src/MimeKit/Cryptography/SubjectIdentifierType.cs @@ -0,0 +1,50 @@ +// +// SubjectIdentifierType.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Cryptography { + /// + /// The method to use for identifying a certificate. + /// + /// + /// The method to use for identifying a certificate. + /// + public enum SubjectIdentifierType { + /// + /// The identifier type is unknown. + /// + Unknown, + + /// + /// Identify the certificate by its Issuer and Serial Number properties. + /// + IssuerAndSerialNumber, + + /// + /// Identify the certificate by the sha1 hash of its public key. + /// + SubjectKeyIdentifier, + } +} diff --git a/src/MimeKit/Cryptography/TemporarySecureMimeContext.cs b/src/MimeKit/Cryptography/TemporarySecureMimeContext.cs new file mode 100644 index 0000000..7400847 --- /dev/null +++ b/src/MimeKit/Cryptography/TemporarySecureMimeContext.cs @@ -0,0 +1,462 @@ +// +// TemporarySecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Store; +using Org.BouncyCastle.Asn1.X509; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME context that does not persist certificates, private keys or CRLs. + /// + /// + /// A is a special S/MIME context that + /// does not use a persistent store for certificates, private keys, or CRLs. + /// Instead, certificates, private keys, and CRLs are maintained in memory only. + /// + public class TemporarySecureMimeContext : BouncyCastleSecureMimeContext + { + readonly Dictionary capabilities; + internal readonly Dictionary keys; + internal readonly List certificates; + readonly HashSet fingerprints; + readonly List crls; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public TemporarySecureMimeContext () + { + capabilities = new Dictionary (StringComparer.Ordinal); + keys = new Dictionary (StringComparer.Ordinal); + certificates = new List (); + fingerprints = new HashSet (); + crls = new List (); + } + + /// + /// Check whether or not a particular mailbox address can be used for signing. + /// + /// + /// Checks whether or not as particular mailbocx address can be used for signing. + /// + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. + /// + /// is null. + /// + public override bool CanSign (MailboxAddress signer) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + AsymmetricKeyParameter key; + + return GetCmsSignerCertificate (signer, out key) != null; + } + + /// + /// Check whether or not the cryptography context can encrypt to a particular recipient. + /// + /// + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. + /// + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. + /// + public override bool CanEncrypt (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + return GetCmsRecipientCertificate (mailbox) != null; + } + + #region implemented abstract members of SecureMimeContext + + /// + /// Gets the X.509 certificate matching the specified selector. + /// + /// + /// Gets the first certificate that matches the specified selector. + /// + /// The certificate on success; otherwise null. + /// The search criteria for the certificate. + protected override X509Certificate GetCertificate (IX509Selector selector) + { + if (selector == null && certificates.Count > 0) + return certificates[0]; + + foreach (var certificate in certificates) { + if (selector.Match (certificate)) + return certificate; + } + + return null; + } + + /// + /// Gets the private key for the certificate matching the specified selector. + /// + /// + /// Gets the private key for the first certificate that matches the specified selector. + /// + /// The private key on success; otherwise null. + /// The search criteria for the private key. + protected override AsymmetricKeyParameter GetPrivateKey (IX509Selector selector) + { + foreach (var certificate in certificates) { + var fingerprint = certificate.GetFingerprint (); + AsymmetricKeyParameter key; + + if (!keys.TryGetValue (fingerprint, out key)) + continue; + + if (selector != null && !selector.Match (certificate)) + continue; + + return key; + } + + return null; + } + + /// + /// Gets the trusted anchors. + /// + /// + /// A trusted anchor is a trusted root-level X.509 certificate, + /// generally issued by a certificate authority (CA). + /// + /// The trusted anchors. + protected override Org.BouncyCastle.Utilities.Collections.HashSet GetTrustedAnchors () + { + var anchors = new Org.BouncyCastle.Utilities.Collections.HashSet (); + + foreach (var certificate in certificates) { + var keyUsage = certificate.GetKeyUsage (); + + if (keyUsage != null && keyUsage[(int) X509KeyUsageBits.KeyCertSign] && certificate.IsSelfSigned ()) + anchors.Add (new TrustAnchor (certificate, null)); + } + + return anchors; + } + + /// + /// Gets the intermediate certificates. + /// + /// + /// An intermediate certificate is any certificate that exists between the root + /// certificate issued by a Certificate Authority (CA) and the certificate at + /// the end of the chain. + /// + /// The intermediate certificates. + protected override IX509Store GetIntermediateCertificates () + { + var intermediates = new X509CertificateStore (); + + foreach (var certificate in certificates) { + var keyUsage = certificate.GetKeyUsage (); + + if (keyUsage != null && keyUsage[(int) X509KeyUsageBits.KeyCertSign] && !certificate.IsSelfSigned ()) + intermediates.Add (certificate); + } + + return intermediates; + } + + /// + /// Gets the certificate revocation lists. + /// + /// + /// A Certificate Revocation List (CRL) is a list of certificate serial numbers issued + /// by a particular Certificate Authority (CA) that have been revoked, either by the CA + /// itself or by the owner of the revoked certificate. + /// + /// The certificate revocation lists. + protected override IX509Store GetCertificateRevocationLists () + { + return X509StoreFactory.Create ("Crl/Collection", new X509CollectionStoreParameters (crls)); + } + + /// + /// Get the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// + /// Gets the date & time for the next scheduled certificate revocation list update for the specified issuer. + /// + /// The date & time for the next update. + /// The issuer. + protected override DateTime GetNextCertificateRevocationListUpdate (X509Name issuer) + { + var nextUpdate = DateTime.MinValue.ToUniversalTime (); + + foreach (var crl in crls) { + if (!crl.IssuerDN.Equals (issuer)) + continue; + + nextUpdate = crl.NextUpdate.Value > nextUpdate ? crl.NextUpdate.Value : nextUpdate; + } + + return nextUpdate; + } + + X509Certificate GetCmsRecipientCertificate (MailboxAddress mailbox) + { + var secure = mailbox as SecureMailboxAddress; + var now = DateTime.UtcNow; + + foreach (var certificate in certificates) { + if (certificate.NotBefore > now || certificate.NotAfter < now) + continue; + + var keyUsage = certificate.GetKeyUsageFlags (); + if (keyUsage != 0 && (keyUsage & X509KeyUsageFlags.KeyEncipherment) == 0) + continue; + + if (secure != null) { + var fingerprint = certificate.GetFingerprint (); + + if (!fingerprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) + continue; + } else { + var address = certificate.GetSubjectEmailAddress (); + + if (!address.Equals (mailbox.Address, StringComparison.OrdinalIgnoreCase)) + continue; + } + + return certificate; + } + + return null; + } + + /// + /// Gets the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate certificate and + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address. + /// + /// A . + /// The mailbox. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox) + { + X509Certificate certificate; + + if ((certificate = GetCmsRecipientCertificate (mailbox)) == null) + throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found."); + + var recipient = new CmsRecipient (certificate); + EncryptionAlgorithm[] algorithms; + + if (capabilities.TryGetValue (certificate.GetFingerprint (), out algorithms)) + recipient.EncryptionAlgorithms = algorithms; + + return recipient; + } + + X509Certificate GetCmsSignerCertificate (MailboxAddress mailbox, out AsymmetricKeyParameter key) + { + var secure = mailbox as SecureMailboxAddress; + var now = DateTime.UtcNow; + + foreach (var certificate in certificates) { + if (certificate.NotBefore > now || certificate.NotAfter < now) + continue; + + var keyUsage = certificate.GetKeyUsageFlags (); + if (keyUsage != 0 && (keyUsage & DigitalSignatureKeyUsageFlags) == 0) + continue; + + var fingerprint = certificate.GetFingerprint (); + + if (!keys.TryGetValue (fingerprint, out key)) + continue; + + if (secure != null) { + if (!fingerprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) + continue; + } else { + var address = certificate.GetSubjectEmailAddress (); + + if (!address.Equals (mailbox.Address, StringComparison.OrdinalIgnoreCase)) + continue; + } + + return certificate; + } + + key = null; + + return null; + } + + /// + /// Gets the for the specified mailbox. + /// + /// + /// Constructs a with the appropriate signing certificate + /// for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address for database lookups. + /// + /// A . + /// The mailbox. + /// The preferred digest algorithm. + /// + /// A certificate for the specified could not be found. + /// + protected override CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo) + { + X509Certificate certificate; + AsymmetricKeyParameter key; + + if ((certificate = GetCmsSignerCertificate (mailbox, out key)) == null) + throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found."); + + return new CmsSigner (BuildCertificateChain (certificate), key) { + DigestAlgorithm = digestAlgo + }; + } + + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// The certificate. + /// The encryption algorithm capabilities of the client (in preferred order). + /// The timestamp. + protected override void UpdateSecureMimeCapabilities (X509Certificate certificate, EncryptionAlgorithm[] algorithms, DateTime timestamp) + { + capabilities[certificate.GetFingerprint ()] = algorithms; + } + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// The raw certificate and key data in pkcs12 format. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + public override void Import (Stream stream, string password) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ()); + + foreach (string alias in pkcs12.Aliases) { + if (pkcs12.IsKeyEntry (alias)) { + var chain = pkcs12.GetCertificateChain (alias); + var entry = pkcs12.GetKey (alias); + + for (int i = 0; i < chain.Length; i++) + Import (chain[i].Certificate); + + var fingerprint = chain[0].Certificate.GetFingerprint (); + if (!keys.ContainsKey (fingerprint)) + keys.Add (fingerprint, entry.Key); + } else if (pkcs12.IsCertificateEntry (alias)) { + var entry = pkcs12.GetCertificate (alias); + + Import (entry.Certificate); + } + } + } + + /// + /// Imports the specified certificate. + /// + /// + /// Imports the specified certificate. + /// + /// The certificate. + /// + /// is null. + /// + public override void Import (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (fingerprints.Add (certificate.GetFingerprint ())) + certificates.Add (certificate); + } + + /// + /// Imports the specified certificate revocation list. + /// + /// + /// Imports the specified certificate revocation list. + /// + /// The certificate revocation list. + /// + /// is null. + /// + public override void Import (X509Crl crl) + { + if (crl == null) + throw new ArgumentNullException (nameof (crl)); + + crls.Add (crl); + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/WindowsSecureMimeContext.cs b/src/MimeKit/Cryptography/WindowsSecureMimeContext.cs new file mode 100644 index 0000000..6d78974 --- /dev/null +++ b/src/MimeKit/Cryptography/WindowsSecureMimeContext.cs @@ -0,0 +1,1248 @@ +// +// WindowsSecureMimeContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography.X509Certificates; + +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.Smime; + +using RealCmsSigner = System.Security.Cryptography.Pkcs.CmsSigner; +using RealCmsRecipient = System.Security.Cryptography.Pkcs.CmsRecipient; +using RealAlgorithmIdentifier = System.Security.Cryptography.Pkcs.AlgorithmIdentifier; +using RealSubjectIdentifierType = System.Security.Cryptography.Pkcs.SubjectIdentifierType; +using RealCmsRecipientCollection = System.Security.Cryptography.Pkcs.CmsRecipientCollection; +using RealX509KeyUsageFlags = System.Security.Cryptography.X509Certificates.X509KeyUsageFlags; + +using MimeKit.IO; + +namespace MimeKit.Cryptography { + /// + /// A Secure MIME (S/MIME) cryptography context. + /// + /// + /// An S/MIME cryptography context that uses + /// for certificate storage and retrieval. + /// + public class WindowsSecureMimeContext : SecureMimeContext + { + const X509KeyStorageFlags DefaultKeyStorageFlags = X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The X.509 store location. + public WindowsSecureMimeContext (StoreLocation location) + { + StoreLocation = location; + + // System.Security does not support Camellia... + Disable (EncryptionAlgorithm.Camellia256); + Disable (EncryptionAlgorithm.Camellia192); + Disable (EncryptionAlgorithm.Camellia192); + + // ... or Blowfish/Twofish... + Disable (EncryptionAlgorithm.Blowfish); + Disable (EncryptionAlgorithm.Twofish); + + // ...or CAST5... + Disable (EncryptionAlgorithm.Cast5); + + // ...or IDEA... + Disable (EncryptionAlgorithm.Idea); + + // ...or SEED... + Disable (EncryptionAlgorithm.Seed); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Constructs an S/MIME context using the current user's X.509 store location. + /// + public WindowsSecureMimeContext () : this (StoreLocation.CurrentUser) + { + } + + /// + /// Gets the X.509 store location. + /// + /// + /// Gets the X.509 store location. + /// + /// The store location. + public StoreLocation StoreLocation { + get; private set; + } + + /// + /// Check whether or not a particular mailbox address can be used for signing. + /// + /// + /// Checks whether or not as particular mailbocx address can be used for signing. + /// + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. + /// + /// is null. + /// + public override bool CanSign (MailboxAddress signer) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + return GetSignerCertificate (signer) != null; + } + + /// + /// Check whether or not the cryptography context can encrypt to a particular recipient. + /// + /// + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. + /// + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. + /// + public override bool CanEncrypt (MailboxAddress mailbox) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + return GetRecipientCertificate (mailbox) != null; + } + + #region implemented abstract members of SecureMimeContext + + /// + /// Get the certificate for the specified recipient. + /// + /// + /// Gets the certificate for the specified recipient. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address. + /// + /// The certificate to use for the recipient; otherwise, or null. + /// The recipient's mailbox address. + protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailbox) + { + var storeNames = new [] { StoreName.AddressBook, StoreName.My, StoreName.TrustedPeople }; + var secure = mailbox as SecureMailboxAddress; + var now = DateTime.UtcNow; + + foreach (var storeName in storeNames) { + var store = new X509Store (storeName, StoreLocation); + + store.Open (OpenFlags.ReadOnly); + + try { + foreach (var certificate in store.Certificates) { + if (certificate.NotBefore > now || certificate.NotAfter < now) + continue; + + var usage = certificate.Extensions[X509Extensions.KeyUsage.Id] as X509KeyUsageExtension; + if (usage != null && (usage.KeyUsages & RealX509KeyUsageFlags.KeyEncipherment) == 0) + continue; + + if (secure != null) { + if (!certificate.Thumbprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) + continue; + } else { + var address = certificate.GetNameInfo (X509NameType.EmailName, false); + + if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase)) + continue; + } + + return certificate; + } + } finally { + store.Close (); + } + } + + return null; + } + + /// + /// Get the for the specified mailbox. + /// + /// + /// Constructs a with + /// the appropriate certificate for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address. + /// + /// A . + /// The recipient's mailbox address. + /// + /// A certificate for the specified could not be found. + /// + protected virtual RealCmsRecipient GetCmsRecipient (MailboxAddress mailbox) + { + X509Certificate2 certificate; + + if ((certificate = GetRecipientCertificate (mailbox)) == null) + throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found."); + + return new RealCmsRecipient (certificate); + } + + /// + /// Get a collection of for the specified mailboxes. + /// + /// + /// Gets a collection of for the specified mailboxes. + /// + /// A . + /// The recipient mailboxes. + /// + /// is null. + /// + /// + /// A certificate for one or more of the specified could not be found. + /// + RealCmsRecipientCollection GetCmsRecipients (IEnumerable mailboxes) + { + var collection = new RealCmsRecipientCollection (); + + foreach (var recipient in mailboxes) + collection.Add (GetCmsRecipient (recipient)); + + if (collection.Count == 0) + throw new ArgumentException ("No recipients specified.", nameof (mailboxes)); + + return collection; + } + + RealCmsRecipientCollection GetCmsRecipients (CmsRecipientCollection recipients) + { + var collection = new RealCmsRecipientCollection (); + + foreach (var recipient in recipients) { + var certificate = new X509Certificate2 (recipient.Certificate.GetEncoded ()); + RealSubjectIdentifierType type; + RealCmsRecipient real; + + if (recipient.RecipientIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + type = RealSubjectIdentifierType.IssuerAndSerialNumber; + else + type = RealSubjectIdentifierType.SubjectKeyIdentifier; + +#if NETCOREAPP3_0 + var padding = recipient.RsaEncryptionPadding?.AsRSAEncryptionPadding (); + + if (padding != null) + real = new RealCmsRecipient (type, certificate, padding); + else + real = new RealCmsRecipient (type, certificate); +#else + if (recipient.RsaEncryptionPadding?.Scheme == RsaEncryptionPaddingScheme.Oaep) + throw new NotSupportedException ("The RSAES-OAEP encryption padding scheme is not supported by the WindowsSecureMimeContext. You must use a subclass of BouncyCastleSecureMimeContext to get this feature."); + + real = new RealCmsRecipient (type, certificate); +#endif + + collection.Add (real); + } + + return collection; + } + + /// + /// Get the certificate for the specified signer. + /// + /// + /// Gets the certificate for the specified signer. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address. + /// + /// The certificate to use for the signer; otherwise, or null. + /// The signer's mailbox address. + protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox) + { + var store = new X509Store (StoreName.My, StoreLocation); + var secure = mailbox as SecureMailboxAddress; + var now = DateTime.UtcNow; + + store.Open (OpenFlags.ReadOnly); + + try { + foreach (var certificate in store.Certificates) { + if (certificate.NotBefore > now || certificate.NotAfter < now) + continue; + + var usage = certificate.Extensions[X509Extensions.KeyUsage.Id] as X509KeyUsageExtension; + if (usage != null && (usage.KeyUsages & (RealX509KeyUsageFlags.DigitalSignature | RealX509KeyUsageFlags.NonRepudiation)) == 0) + continue; + + if (!certificate.HasPrivateKey) + continue; + + if (secure != null) { + if (!certificate.Thumbprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) + continue; + } else { + var address = certificate.GetNameInfo (X509NameType.EmailName, false); + + if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase)) + continue; + } + + return certificate; + } + } finally { + store.Close (); + } + + return null; + } + + AsnEncodedData GetSecureMimeCapabilities () + { + var attr = GetSecureMimeCapabilitiesAttribute (false); + + return new AsnEncodedData (attr.AttrType.Id, attr.AttrValues[0].GetEncoded ()); + } + + RealCmsSigner GetRealCmsSigner (RealSubjectIdentifierType type, X509Certificate2 certificate, DigestAlgorithm digestAlgo) + { + var signer = new RealCmsSigner (type, certificate); + signer.DigestAlgorithm = new Oid (GetDigestOid (digestAlgo)); + signer.SignedAttributes.Add (GetSecureMimeCapabilities ()); + signer.SignedAttributes.Add (new Pkcs9SigningTime ()); + signer.IncludeOption = X509IncludeOption.ExcludeRoot; + return signer; + } + + RealCmsSigner GetRealCmsSigner (CmsSigner signer) + { + if (signer.RsaSignaturePadding == RsaSignaturePadding.Pss) + throw new NotSupportedException ("The RSASSA-PSS signature padding scheme is not supported by the WindowsSecureMimeContext. You must use a subclass of BouncyCastleSecureMimeContext to get this feature."); + + var certificate = signer.Certificate.AsX509Certificate2 (); + RealSubjectIdentifierType type; + + if (signer.SignerIdentifierType != SubjectIdentifierType.SubjectKeyIdentifier) + type = RealSubjectIdentifierType.IssuerAndSerialNumber; + else + type = RealSubjectIdentifierType.SubjectKeyIdentifier; + + certificate.PrivateKey = signer.PrivateKey.AsAsymmetricAlgorithm (); + + return GetRealCmsSigner (type, certificate, signer.DigestAlgorithm); + } + + /// + /// Get the for the specified mailbox. + /// + /// + /// Constructs a with + /// the appropriate signing certificate for the specified mailbox. + /// If the mailbox is a , the + /// property will be used instead of + /// the mailbox address for database lookups. + /// + /// A . + /// The signer's mailbox address. + /// The preferred digest algorithm. + /// + /// A certificate for the specified could not be found. + /// + protected virtual RealCmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo) + { + X509Certificate2 certificate; + + if ((certificate = GetSignerCertificate (mailbox)) == null) + throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found."); + + return GetRealCmsSigner (RealSubjectIdentifierType.IssuerAndSerialNumber, certificate, digestAlgo); + } + + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// + /// + /// Updates the known S/MIME capabilities of the client used by the recipient that owns the specified certificate. + /// This method is called when decoding digital signatures that include S/MIME capabilities in the metadata, allowing custom + /// implementations to update the X.509 certificate records with the list of preferred encryption algorithms specified by the + /// sending client. + /// + /// The certificate. + /// The encryption algorithm capabilities of the client (in preferred order). + /// The timestamp. + protected virtual void UpdateSecureMimeCapabilities (X509Certificate2 certificate, EncryptionAlgorithm[] algorithms, DateTime timestamp) + { + // TODO: implement this - should we add/update the X509Extension for S/MIME Capabilities? + } + + static byte[] ReadAllBytes (Stream stream) + { + if (stream is MemoryBlockStream) + return ((MemoryBlockStream) stream).ToArray (); + + if (stream is MemoryStream) + return ((MemoryStream) stream).ToArray (); + + using (var memory = new MemoryBlockStream ()) { + stream.CopyTo (memory, 4096); + return memory.ToArray (); + } + } + + static async Task ReadAllBytesAsync (Stream stream, CancellationToken cancellationToken) + { + if (stream is MemoryBlockStream) + return ((MemoryBlockStream) stream).ToArray (); + + if (stream is MemoryStream) + return ((MemoryStream) stream).ToArray (); + + using (var memory = new MemoryBlockStream ()) { + await stream.CopyToAsync (memory, 4096, cancellationToken).ConfigureAwait (false); + return memory.ToArray (); + } + } + + Stream Sign (RealCmsSigner signer, Stream content, bool detach) + { + var contentInfo = new ContentInfo (ReadAllBytes (content)); + var signed = new SignedCms (contentInfo, detach); + + try { + signed.ComputeSignature (signer, false); + } catch (CryptographicException) { + signer.IncludeOption = X509IncludeOption.EndCertOnly; + signed.ComputeSignature (signer, false); + } + + var signedData = signed.Encode (); + + return new MemoryStream (signedData, false); + } + + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// + /// Cryptographically signs and encapsulates the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime EncapsulatedSign (CmsSigner signer, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var real = GetRealCmsSigner (signer); + + return new ApplicationPkcs7Mime (SecureMimeType.SignedData, Sign (real, content, false)); + } + + /// + /// Sign and encapsulate the content using the specified signer. + /// + /// + /// Sign and encapsulate the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime EncapsulatedSign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var real = GetCmsSigner (signer, digestAlgo); + + return new ApplicationPkcs7Mime (SecureMimeType.SignedData, Sign (real, content, false)); + } + + /// + /// Cryptographically signs the content using the specified signer. + /// + /// + /// Cryptographically signs the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Signature Sign (CmsSigner signer, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var real = GetRealCmsSigner (signer); + + return new ApplicationPkcs7Signature (Sign (real, content, true)); + } + + /// + /// Sign the content using the specified signer. + /// + /// + /// Sign the content using the specified signer. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing certificate could not be found for . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var cmsSigner = GetCmsSigner (signer, digestAlgo); + + return new ApplicationPkcs7Signature (Sign (cmsSigner, content, true)); + } + + /// + /// Attempts to map a + /// to a . + /// + /// + /// Attempts to map a + /// to a . + /// + /// true if the algorithm identifier was successfully mapped; otherwise, false. + /// The algorithm identifier. + /// The encryption algorithm. + /// + /// is null. + /// + internal protected static bool TryGetDigestAlgorithm (Oid identifier, out DigestAlgorithm algorithm) + { + if (identifier == null) + throw new ArgumentNullException (nameof (identifier)); + + return TryGetDigestAlgorithm (identifier.Value, out algorithm); + } + + DigitalSignatureCollection GetDigitalSignatures (SignedCms signed) + { + var signatures = new List (); + + foreach (var signerInfo in signed.SignerInfos) { + var signature = new WindowsSecureMimeDigitalSignature (signerInfo); + + if (signature.EncryptionAlgorithms.Length > 0 && signature.CreationDate.Ticks != 0) { + UpdateSecureMimeCapabilities (signerInfo.Certificate, signature.EncryptionAlgorithms, signature.CreationDate); + } else { + try { + Import (signerInfo.Certificate); + } catch { + } + } + + signatures.Add (signature); + } + + return new DigitalSignatureCollection (signatures); + } + + /// + /// Verify the specified content using the detached signature data. + /// + /// + /// Verifies the specified content using the detached signature data. + /// + /// A list of the digital signatures. + /// The content. + /// The detached signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + var contentInfo = new ContentInfo (ReadAllBytes (content)); + var signed = new SignedCms (contentInfo, true); + + signed.Decode (ReadAllBytes (signatureData)); + + return GetDigitalSignatures (signed); + } + + /// + /// Asynchronously verify the specified content using the detached signature data. + /// + /// + /// Verifies the specified content using the detached signature data. + /// + /// A list of the digital signatures. + /// The content. + /// The detached signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override async Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + var contentInfo = new ContentInfo (await ReadAllBytesAsync (content, cancellationToken).ConfigureAwait (false)); + var signed = new SignedCms (contentInfo, true); + + signed.Decode (await ReadAllBytesAsync (signatureData, cancellationToken).ConfigureAwait (false)); + + return GetDigitalSignatures (signed); + } + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The list of digital signatures. + /// The signed data. + /// The extracted MIME entity. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The extracted content could not be parsed as a MIME entity. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override DigitalSignatureCollection Verify (Stream signedData, out MimeEntity entity, CancellationToken cancellationToken = default (CancellationToken)) + { + if (signedData == null) + throw new ArgumentNullException (nameof (signedData)); + + var content = ReadAllBytes (signedData); + var signed = new SignedCms (); + + signed.Decode (content); + + var memory = new MemoryStream (signed.ContentInfo.Content, false); + + try { + entity = MimeEntity.Load (memory, true, cancellationToken); + } catch { + memory.Dispose (); + throw; + } + + return GetDigitalSignatures (signed); + } + + /// + /// Verify the digital signatures of the specified signed data and extract the original content. + /// + /// + /// Verifies the digital signatures of the specified signed data and extracts the original content. + /// + /// The extracted content stream. + /// The signed data. + /// The digital signatures. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override Stream Verify (Stream signedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + if (signedData == null) + throw new ArgumentNullException (nameof (signedData)); + + var content = ReadAllBytes (signedData); + var signed = new SignedCms (); + + signed.Decode (content); + + signatures = GetDigitalSignatures (signed); + + return new MemoryStream (signed.ContentInfo.Content, false); + } + + /// + /// Gets the preferred encryption algorithm to use for encrypting to the specified recipients. + /// + /// + /// Gets the preferred encryption algorithm to use for encrypting to the specified recipients + /// based on the encryption algorithms supported by each of the recipients, the + /// , and the + /// . + /// If the supported encryption algorithms are unknown for any recipient, it is assumed that + /// the recipient supports at least the Triple-DES encryption algorithm. + /// + /// The preferred encryption algorithm. + /// The recipients. + protected virtual EncryptionAlgorithm GetPreferredEncryptionAlgorithm (RealCmsRecipientCollection recipients) + { + var votes = new int[EncryptionAlgorithmCount]; + int need = recipients.Count; + + foreach (var recipient in recipients) { + var supported = recipient.Certificate.GetEncryptionAlgorithms (); + + foreach (var algorithm in supported) + votes[(int) algorithm]++; + } + + // Starting with S/MIME v3 (published in 1999), Triple-DES is a REQUIRED algorithm. + // S/MIME v2.x and older only required RC2/40, but SUGGESTED Triple-DES. + // Considering the fact that Bruce Schneier was able to write a + // screensaver that could crack RC2/40 back in the late 90's, let's + // not default to anything weaker than Triple-DES... + EncryptionAlgorithm chosen = EncryptionAlgorithm.TripleDes; + int nvotes = 0; + + votes[(int) EncryptionAlgorithm.TripleDes] = need; + + // iterate through the algorithms, from strongest to weakest, keeping track + // of the algorithm with the most amount of votes (between algorithms with + // the same number of votes, choose the strongest of the 2 - i.e. the one + // that we arrive at first). + var algorithms = EncryptionAlgorithmRank; + for (int i = 0; i < algorithms.Length; i++) { + var algorithm = algorithms[i]; + + if (!IsEnabled (algorithm)) + continue; + + if (votes[(int) algorithm] > nvotes) { + nvotes = votes[(int) algorithm]; + chosen = algorithm; + } + } + + return chosen; + } + + internal RealAlgorithmIdentifier GetAlgorithmIdentifier (EncryptionAlgorithm algorithm) + { + switch (algorithm) { + case EncryptionAlgorithm.Aes256: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.Aes256Cbc)); + case EncryptionAlgorithm.Aes192: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.Aes192Cbc)); + case EncryptionAlgorithm.Aes128: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.Aes128Cbc)); + case EncryptionAlgorithm.TripleDes: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.DesEde3Cbc)); + case EncryptionAlgorithm.Des: + return new RealAlgorithmIdentifier (new Oid (SmimeCapability.DesCbc.Id)); + case EncryptionAlgorithm.RC2128: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.RC2Cbc), 128); + case EncryptionAlgorithm.RC264: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.RC2Cbc), 64); + case EncryptionAlgorithm.RC240: + return new RealAlgorithmIdentifier (new Oid (CmsEnvelopedGenerator.RC2Cbc), 40); + default: + throw new NotSupportedException (string.Format ("The {0} encryption algorithm is not supported by the {1}.", algorithm, GetType ().Name)); + } + } + + Stream Envelope (RealCmsRecipientCollection recipients, Stream content, EncryptionAlgorithm encryptionAlgorithm) + { + var contentInfo = new ContentInfo (ReadAllBytes (content)); + var algorithm = GetAlgorithmIdentifier (encryptionAlgorithm); + var envelopedData = new EnvelopedCms (contentInfo, algorithm); + + envelopedData.Encrypt (recipients); + + return new MemoryStream (envelopedData.Encode (), false); + } + + Stream Envelope (RealCmsRecipientCollection recipients, Stream content) + { + var algorithm = GetPreferredEncryptionAlgorithm (recipients); + + return Envelope (recipients, content, algorithm); + } + + Stream Envelope (CmsRecipientCollection recipients, Stream content) + { + var algorithm = GetPreferredEncryptionAlgorithm (recipients); + + return Envelope (GetCmsRecipients (recipients), content, algorithm); + } + + /// + /// Encrypts the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted content. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override ApplicationPkcs7Mime Encrypt (CmsRecipientCollection recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + return new ApplicationPkcs7Mime (SecureMimeType.EnvelopedData, Envelope (recipients, content)); + } + + /// + /// Encrypts the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// A certificate for one or more of the could not be found. + /// + /// + /// A certificate could not be found for one or more of the . + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Encrypt (IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var real = GetCmsRecipients (recipients); + + return new ApplicationPkcs7Mime (SecureMimeType.EnvelopedData, Envelope (real, content)); + } + + /// + /// Decrypt the encrypted data. + /// + /// + /// Decrypt the encrypted data. + /// + /// The decrypted . + /// The encrypted data. + /// The cancellation token. + /// + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + public override MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + var enveloped = new EnvelopedCms (); + CryptographicException ce = null; + + enveloped.Decode (ReadAllBytes (encryptedData)); + + foreach (var recipient in enveloped.RecipientInfos) { + try { + enveloped.Decrypt (recipient); + ce = null; + break; + } catch (CryptographicException ex) { + ce = ex; + } + } + + if (ce != null) + throw ce; + + var decryptedData = enveloped.Encode (); + + var memory = new MemoryStream (decryptedData, false); + + return MimeEntity.Load (memory, true, cancellationToken); + } + + /// + /// Decrypts the specified encryptedData to an output stream. + /// + /// + /// Decrypts the specified encryptedData to an output stream. + /// + /// The encrypted data. + /// The decrypted data. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override void DecryptTo (Stream encryptedData, Stream decryptedData) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + if (decryptedData == null) + throw new ArgumentNullException (nameof (decryptedData)); + + var enveloped = new EnvelopedCms (); + + enveloped.Decode (ReadAllBytes (encryptedData)); + enveloped.Decrypt (); + + var encoded = enveloped.Encode (); + + decryptedData.Write (encoded, 0, encoded.Length); + } + + /// + /// Import the specified certificate. + /// + /// + /// Import the specified certificate. + /// + /// The store to import the certificate into. + /// The certificate. + /// + /// is null. + /// + public void Import (StoreName storeName, X509Certificate2 certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var store = new X509Store (storeName, StoreLocation); + + store.Open (OpenFlags.ReadWrite); + store.Add (certificate); + store.Close (); + } + + /// + /// Import the specified certificate. + /// + /// + /// Imports the specified certificate into the store. + /// + /// The certificate. + /// + /// is null. + /// + public void Import (X509Certificate2 certificate) + { + Import (StoreName.AddressBook, certificate); + } + + /// + /// Import the specified certificate. + /// + /// + /// Import the specified certificate. + /// + /// The store to import the certificate into. + /// The certificate. + /// + /// is null. + /// + public void Import (StoreName storeName, Org.BouncyCastle.X509.X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + Import (storeName, new X509Certificate2 (certificate.GetEncoded ())); + } + + /// + /// Import the specified certificate. + /// + /// + /// Imports the specified certificate into the store. + /// + /// The certificate. + /// + /// is null. + /// + public override void Import (Org.BouncyCastle.X509.X509Certificate certificate) + { + Import (StoreName.AddressBook, certificate); + } + + /// + /// Import the specified certificate revocation list. + /// + /// + /// Import the specified certificate revocation list. + /// + /// The certificate revocation list. + /// + /// is null. + /// + public override void Import (X509Crl crl) + { + if (crl == null) + throw new ArgumentNullException (nameof (crl)); + + foreach (Org.BouncyCastle.X509.X509Certificate certificate in crl.GetRevokedCertificates ()) + Import (StoreName.Disallowed, certificate); + } + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// The raw certificate and key data. + /// The password to unlock the stream. + /// The storage flags to use when importing the certificate and private key. + /// + /// is null. + /// -or- + /// is null. + /// + public void Import (Stream stream, string password, X509KeyStorageFlags flags) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var rawData = ReadAllBytes (stream); + var store = new X509Store (StoreName.My, StoreLocation); + var certs = new X509Certificate2Collection (); + + store.Open (OpenFlags.ReadWrite); + certs.Import (rawData, password, flags); + store.AddRange (certs); + store.Close (); + } + + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// + /// Imports certificates and keys from a pkcs12-encoded stream. + /// + /// The raw certificate and key data. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + public override void Import (Stream stream, string password) + { + Import (stream, password, DefaultKeyStorageFlags); + } + + /// + /// Exports the certificates for the specified mailboxes. + /// + /// + /// Exports the certificates for the specified mailboxes. + /// + /// A new instance containing + /// the exported keys. + /// The mailboxes. + /// + /// is null. + /// + /// + /// No mailboxes were specified. + /// + /// + /// A certificate for one or more of the could not be found. + /// + /// + /// An error occurred in the cryptographic message syntax subsystem. + /// + public override MimePart Export (IEnumerable mailboxes) + { + if (mailboxes == null) + throw new ArgumentNullException (nameof (mailboxes)); + + var certificates = new X509CertificateStore (); + int count = 0; + + foreach (var mailbox in mailboxes) { + var certificate = GetRecipientCertificate (mailbox); + + if (certificate != null) + certificates.Add (certificate.AsBouncyCastleCertificate ()); + + count++; + } + + if (count == 0) + throw new ArgumentException ("No mailboxes specified.", nameof (mailboxes)); + + var cms = new CmsSignedDataStreamGenerator (); + cms.AddCertificates (certificates); + + var memory = new MemoryBlockStream (); + cms.Open (memory).Dispose (); + memory.Position = 0; + + return new ApplicationPkcs7Mime (SecureMimeType.CertsOnly, memory); + } +#endregion + } +} diff --git a/src/MimeKit/Cryptography/WindowsSecureMimeDigitalCertificate.cs b/src/MimeKit/Cryptography/WindowsSecureMimeDigitalCertificate.cs new file mode 100644 index 0000000..c95b823 --- /dev/null +++ b/src/MimeKit/Cryptography/WindowsSecureMimeDigitalCertificate.cs @@ -0,0 +1,148 @@ +// +// WindowsSecureMimeDigitalCertificate.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace MimeKit.Cryptography { + /// + /// An S/MIME digital certificate. + /// + /// + /// An S/MIME digital certificate that is used with the . + /// + public class WindowsSecureMimeDigitalCertificate : IDigitalCertificate + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// An X.509 certificate. + /// + /// is null. + /// + public WindowsSecureMimeDigitalCertificate (X509Certificate2 certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + Certificate = certificate; + PublicKeyAlgorithm = certificate.GetPublicKeyAlgorithm (); + } + + /// + /// Get the X.509 certificate. + /// + /// + /// Gets the X.509 certificate. + /// + /// The certificate. + public X509Certificate2 Certificate { + get; private set; + } + +// /// +// /// Gets the chain status. +// /// +// /// The chain status. +// public X509ChainStatusFlags ChainStatus { +// get; internal set; +// } + + #region IDigitalCertificate implementation + + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// + /// Gets the public key algorithm supported by the certificate. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get; private set; + } + + /// + /// Gets the date that the certificate was created. + /// + /// + /// Gets the date that the certificate was created. + /// + /// The creation date. + public DateTime CreationDate { + get { return Certificate.NotBefore.ToUniversalTime (); } + } + + /// + /// Gets the expiration date of the certificate. + /// + /// + /// Gets the expiration date of the certificate. + /// + /// The expiration date. + public DateTime ExpirationDate { + get { return Certificate.NotAfter.ToUniversalTime (); } + } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// Gets the fingerprint of the certificate. + /// + /// The fingerprint. + public string Fingerprint { + get { return Certificate.Thumbprint; } + } + + /// + /// Gets the email address of the owner of the certificate. + /// + /// + /// Gets the email address of the owner of the certificate. + /// + /// The email address. + public string Email { + get { return Certificate.GetNameInfo (X509NameType.EmailName, false); } + } + + /// + /// Gets the name of the owner of the certificate. + /// + /// + /// Gets the name of the owner of the certificate. + /// + /// The name of the owner. + public string Name { + get { return Certificate.GetNameInfo (X509NameType.SimpleName, false); } + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/WindowsSecureMimeDigitalSignature.cs b/src/MimeKit/Cryptography/WindowsSecureMimeDigitalSignature.cs new file mode 100644 index 0000000..7754ab3 --- /dev/null +++ b/src/MimeKit/Cryptography/WindowsSecureMimeDigitalSignature.cs @@ -0,0 +1,212 @@ +// +// WindowsSecureMimeDigitalSignature.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Security.Cryptography.Pkcs; + +using Org.BouncyCastle.Asn1; + +using CmsAttributes = Org.BouncyCastle.Asn1.Cms.CmsAttributes; +using SmimeAttributes = Org.BouncyCastle.Asn1.Smime.SmimeAttributes; + +namespace MimeKit.Cryptography +{ + /// + /// An S/MIME digital signature. + /// + /// + /// An S/MIME digital signature that is used with the . + /// + public class WindowsSecureMimeDigitalSignature : IDigitalSignature + { + DigitalSignatureVerifyException vex; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The information about the signer. + /// + /// is null. + /// + public WindowsSecureMimeDigitalSignature (SignerInfo signerInfo) + { + if (signerInfo == null) + throw new ArgumentNullException (nameof (signerInfo)); + + SignerInfo = signerInfo; + + var algorithms = new List (); + DigestAlgorithm digestAlgo; + + if (signerInfo.SignedAttributes != null) { + for (int i = 0; i < signerInfo.SignedAttributes.Count; i++) { + if (signerInfo.SignedAttributes[i].Oid.Value == CmsAttributes.SigningTime.Id) { + var signingTime = signerInfo.SignedAttributes[i].Values[0] as Pkcs9SigningTime; + + if (signingTime != null) + CreationDate = signingTime.SigningTime; + } else if (signerInfo.SignedAttributes[i].Oid.Value == SmimeAttributes.SmimeCapabilities.Id) { + foreach (var value in signerInfo.SignedAttributes[i].Values) { + var sequences = (DerSequence) Asn1Object.FromByteArray (value.RawData); + + foreach (Asn1Sequence sequence in sequences) { + var identifier = Org.BouncyCastle.Asn1.X509.AlgorithmIdentifier.GetInstance (sequence); + EncryptionAlgorithm algorithm; + + if (BouncyCastleSecureMimeContext.TryGetEncryptionAlgorithm (identifier, out algorithm)) + algorithms.Add (algorithm); + } + } + } + } + } + + EncryptionAlgorithms = algorithms.ToArray (); + + if (WindowsSecureMimeContext.TryGetDigestAlgorithm (signerInfo.DigestAlgorithm, out digestAlgo)) + DigestAlgorithm = digestAlgo; + + SignerCertificate = new WindowsSecureMimeDigitalCertificate (signerInfo.Certificate); + } + + /// + /// Gets the signer info. + /// + /// + /// Gets the signer info. + /// + /// The signer info. + public SignerInfo SignerInfo { + get; private set; + } + + /// + /// Gets the list of encryption algorithms, in preferential order, + /// that the signer's client supports. + /// + /// + /// Gets the list of encryption algorithms, in preferential order, + /// that the signer's client supports. + /// + /// The S/MIME encryption algorithms. + public EncryptionAlgorithm[] EncryptionAlgorithms { + get; private set; + } + + #region IDigitalSignature implementation + + /// + /// Gets certificate used by the signer. + /// + /// + /// Gets certificate used by the signer. + /// + /// The signer's certificate. + public IDigitalCertificate SignerCertificate { + get; private set; + } + + /// + /// Gets the public key algorithm used for the signature. + /// + /// + /// Gets the public key algorithm used for the signature. + /// + /// The public key algorithm. + public PublicKeyAlgorithm PublicKeyAlgorithm { + get { return SignerCertificate != null ? SignerCertificate.PublicKeyAlgorithm : PublicKeyAlgorithm.None; } + } + + /// + /// Gets the digest algorithm used for the signature. + /// + /// + /// Gets the digest algorithm used for the signature. + /// + /// The digest algorithm. + public DigestAlgorithm DigestAlgorithm { + get; private set; + } + + /// + /// Gets the creation date of the digital signature. + /// + /// + /// Gets the creation date of the digital signature. + /// + /// The creation date in coordinated universal time (UTC). + public DateTime CreationDate { + get; private set; + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if the signature is valid; otherwise false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify () + { + return Verify (false); + } + + /// + /// Verifies the digital signature. + /// + /// + /// Verifies the digital signature. + /// + /// true if only the signature itself should be verified; otherwise, both the signature and the certificate chain are validated. + /// true if the signature is valid; otherwise, false. + /// + /// An error verifying the signature has occurred. + /// + public bool Verify (bool verifySignatureOnly) + { + if (vex != null) + throw vex; + + try { + SignerInfo.CheckSignature (verifySignatureOnly); + return true; + } catch (Exception ex) { + var message = string.Format ("Failed to verify digital signature: {0}", ex.Message); + vex = new DigitalSignatureVerifyException (message, ex); + throw vex; + } + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/X509Certificate2Extensions.cs b/src/MimeKit/Cryptography/X509Certificate2Extensions.cs new file mode 100644 index 0000000..d41365c --- /dev/null +++ b/src/MimeKit/Cryptography/X509Certificate2Extensions.cs @@ -0,0 +1,153 @@ +// +// X509Certificate2Extensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; +using System.Security.Cryptography; + +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; + +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; + +namespace MimeKit.Cryptography +{ + /// + /// Extension methods for X509Certificate2. + /// + /// + /// Extension methods for X509Certificate2. + /// + public static class X509Certificate2Extensions + { + /// + /// Convert an X509Certificate2 into a BouncyCastle X509Certificate. + /// + /// + /// Converts an X509Certificate2 into a BouncyCastle X509Certificate. + /// + /// The bouncy castle certificate. + /// The certificate. + /// + /// is null. + /// + public static X509Certificate AsBouncyCastleCertificate (this X509Certificate2 certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var rawData = certificate.GetRawCertData (); + + return new X509CertificateParser ().ReadCertificate (rawData); + } + + /// + /// Gets the public key algorithm for the certificate. + /// + /// + /// Gets the public key algorithm for the ceretificate. + /// + /// The public key algorithm. + /// The certificate. + /// + /// is null. + /// + public static PublicKeyAlgorithm GetPublicKeyAlgorithm (this X509Certificate2 certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + var identifier = certificate.GetKeyAlgorithm (); + var oid = new Oid (identifier); + + switch (oid.FriendlyName) { + case "DSA": return PublicKeyAlgorithm.Dsa; + case "RSA": return PublicKeyAlgorithm.RsaGeneral; + case "ECC": return PublicKeyAlgorithm.EllipticCurve; + case "DH": return PublicKeyAlgorithm.DiffieHellman; + default: return PublicKeyAlgorithm.None; + } + } + + static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (byte[] rawData) + { + using (var memory = new MemoryStream (rawData, false)) { + using (var asn1 = new Asn1InputStream (memory)) { + var algorithms = new List (); + var sequence = asn1.ReadObject () as Asn1Sequence; + + if (sequence == null) + return null; + + for (int i = 0; i < sequence.Count; i++) { + var identifier = AlgorithmIdentifier.GetInstance (sequence[i]); + EncryptionAlgorithm algorithm; + + if (BouncyCastleSecureMimeContext.TryGetEncryptionAlgorithm (identifier, out algorithm)) + algorithms.Add (algorithm); + } + + return algorithms.ToArray (); + } + } + } + + /// + /// Get the encryption algorithms that can be used with an X.509 certificate. + /// + /// + /// Scans the X.509 certificate for the S/MIME capabilities extension. If found, + /// the supported encryption algorithms will be decoded and returned. + /// If no extension can be found, the + /// algorithm is returned. + /// + /// The encryption algorithms. + /// The X.509 certificate. + /// + /// is null. + /// + public static EncryptionAlgorithm[] GetEncryptionAlgorithms (this X509Certificate2 certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + foreach (var extension in certificate.Extensions) { + if (extension.Oid.Value == "1.2.840.113549.1.9.15") { + var algorithms = DecodeEncryptionAlgorithms (extension.RawData); + + if (algorithms != null) + return algorithms; + + break; + } + } + + return new EncryptionAlgorithm[] { EncryptionAlgorithm.TripleDes }; + } + } +} diff --git a/src/MimeKit/Cryptography/X509CertificateChain.cs b/src/MimeKit/Cryptography/X509CertificateChain.cs new file mode 100644 index 0000000..9193fe3 --- /dev/null +++ b/src/MimeKit/Cryptography/X509CertificateChain.cs @@ -0,0 +1,381 @@ +// +// X509CertificateChain.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// An X.509 certificate chain. + /// + /// + /// An X.509 certificate chain. + /// + public class X509CertificateChain : IList, IX509Store + { + readonly List certificates; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new X.509 certificate chain. + /// + public X509CertificateChain () + { + certificates = new List (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new X.509 certificate chain based on the specified collection of certificates. + /// + /// A collection of certificates. + /// + /// is null. + /// + public X509CertificateChain (IEnumerable collection) + { + certificates = new List (collection); + } + + #region IList implementation + + /// + /// Gets the index of the specified certificate within the chain. + /// + /// + /// Finds the index of the specified certificate, if it exists. + /// + /// The index of the specified certificate if found; otherwise -1. + /// The certificate to get the index of. + /// + /// is null. + /// + public int IndexOf (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + return certificates.IndexOf (certificate); + } + + /// + /// Inserts the certificate at the specified index. + /// + /// + /// Inserts the certificate at the specified index in the certificates. + /// + /// The index to insert the certificate. + /// The certificate. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + certificates.Insert (index, certificate); + } + + /// + /// Removes the certificate at the specified index. + /// + /// + /// Removes the certificate at the specified index. + /// + /// The index of the certificate to remove. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index >= certificates.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + certificates.RemoveAt (index); + } + + /// + /// Gets or sets the certificate at the specified index. + /// + /// + /// Gets or sets the certificate at the specified index. + /// + /// The internet certificate at the specified index. + /// The index of the certificate to get or set. + /// + /// is null. + /// + /// + /// is out of range. + /// + public X509Certificate this [int index] { + get { return certificates[index]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + certificates[index] = value; + } + } + #endregion + + #region ICollection implementation + + /// + /// Gets the number of certificates in the chain. + /// + /// + /// Indicates the number of certificates in the chain. + /// + /// The number of certificates. + public int Count { + get { return certificates.Count; } + } + + /// + /// Get a value indicating whether the is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Adds the specified certificate to the chain. + /// + /// + /// Adds the specified certificate to the chain. + /// + /// The certificate. + /// + /// is null. + /// + public void Add (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + certificates.Add (certificate); + } + + /// + /// Adds the specified range of certificates to the chain. + /// + /// + /// Adds the specified range of certificates to the chain. + /// + /// The certificates. + /// + /// is null. + /// + public void AddRange (IEnumerable certificates) + { + if (certificates == null) + throw new ArgumentNullException (nameof (certificates)); + + foreach (var certificate in certificates) + Add (certificate); + } + + /// + /// Clears the certificate chain. + /// + /// + /// Removes all of the certificates from the chain. + /// + public void Clear () + { + certificates.Clear (); + } + + /// + /// Checks if the chain contains the specified certificate. + /// + /// + /// Determines whether or not the certificate chain contains the specified certificate. + /// + /// true if the specified certificate exists; + /// otherwise false. + /// The certificate. + /// + /// is null. + /// + public bool Contains (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + return certificates.Contains (certificate); + } + + /// + /// Copies all of the certificates in the chain to the specified array. + /// + /// + /// Copies all of the certificates within the chain into the array, + /// starting at the specified array index. + /// + /// The array to copy the certificates to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (X509Certificate[] array, int arrayIndex) + { + certificates.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified certificate from the chain. + /// + /// + /// Removes the specified certificate from the chain. + /// + /// true if the certificate was removed; otherwise false. + /// The certificate. + /// + /// is null. + /// + public bool Remove (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + return certificates.Remove (certificate); + } + + /// + /// Removes the specified range of certificates from the chain. + /// + /// + /// Removes the specified range of certificates from the chain. + /// + /// The certificates. + /// + /// is null. + /// + public void RemoveRange (IEnumerable certificates) + { + if (certificates == null) + throw new ArgumentNullException (nameof (certificates)); + + foreach (var certificate in certificates) + Remove (certificate); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of certificates. + /// + /// + /// Gets an enumerator for the list of certificates. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return certificates.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of certificates. + /// + /// + /// Gets an enumerator for the list of certificates. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return certificates.GetEnumerator (); + } + + #endregion + + /// + /// Gets an enumerator of matching X.509 certificates based on the specified selector. + /// + /// + /// Gets an enumerator of matching X.509 certificates based on the specified selector. + /// + /// The matching certificates. + /// The match criteria. + public IEnumerable GetMatches (IX509Selector selector) + { + foreach (var certificate in certificates) { + if (selector == null || selector.Match (certificate)) + yield return certificate; + } + + yield break; + } + + #region IX509Store implementation + + /// + /// Gets a collection of matching X.509 certificates based on the specified selector. + /// + /// + /// Gets a collection of matching X.509 certificates based on the specified selector. + /// + /// The matching certificates. + /// The match criteria. + ICollection IX509Store.GetMatches (IX509Selector selector) + { + var matches = new List (); + + foreach (var certificate in GetMatches (selector)) + matches.Add (certificate); + + return matches; + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/X509CertificateDatabase.cs b/src/MimeKit/Cryptography/X509CertificateDatabase.cs new file mode 100644 index 0000000..2a64837 --- /dev/null +++ b/src/MimeKit/Cryptography/X509CertificateDatabase.cs @@ -0,0 +1,985 @@ +// +// X509CertificateDatabase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Data; +using System.Data.Common; +using System.Collections; +using System.Collections.Generic; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Asn1.BC; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// An X.509 certificate database. + /// + /// + /// An X.509 certificate database is used for storing certificates, metadata related to the certificates + /// (such as encryption algorithms supported by the associated client), certificate revocation lists (CRLs), + /// and private keys. + /// + public abstract class X509CertificateDatabase : IX509CertificateDatabase + { + const X509CertificateRecordFields PrivateKeyFields = X509CertificateRecordFields.Certificate | X509CertificateRecordFields.PrivateKey; + static readonly DerObjectIdentifier DefaultEncryptionAlgorithm = BCObjectIdentifiers.bc_pbe_sha256_pkcs12_aes256_cbc; + const int DefaultMinIterations = 1024; + const int DefaultSaltSize = 20; + + readonly char[] passwd; + + /// + /// Initialize a new instance of the class. + /// + /// + /// The password is used to encrypt and decrypt private keys in the database and cannot be null. + /// + /// The password used for encrypting and decrypting the private keys. + /// + /// is null. + /// + protected X509CertificateDatabase (string password) + { + if (password == null) + throw new ArgumentNullException (nameof (password)); + + EncryptionAlgorithm = DefaultEncryptionAlgorithm; + MinIterations = DefaultMinIterations; + SaltSize = DefaultSaltSize; + + passwd = password.ToCharArray (); + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~X509CertificateDatabase () + { + Dispose (false); + } + + /// + /// Gets or sets the algorithm used for encrypting the private keys. + /// + /// + /// The encryption algorithm should be one of the PBE (password-based encryption) algorithms + /// supported by Bouncy Castle. + /// The default algorithm is SHA-256 + AES256. + /// + /// The encryption algorithm. + protected DerObjectIdentifier EncryptionAlgorithm { + get; set; + } + + /// + /// Gets or sets the minimum iterations. + /// + /// + /// The default minimum number of iterations is 1024. + /// + /// The minimum iterations. + protected int MinIterations { + get; set; + } + + /// + /// Gets or sets the size of the salt. + /// + /// + /// The default salt size is 20. + /// + /// The size of the salt. + protected int SaltSize { + get; set; + } + + static int ReadBinaryBlob (DbDataReader reader, int column, ref byte[] buffer) + { +#if NETSTANDARD1_3 || NETSTANDARD1_6 + buffer = reader.GetFieldValue (column); + return (int) buffer.Length; +#else + long nread; + + // first, get the length of the buffer needed + if ((nread = reader.GetBytes (column, 0, null, 0, buffer.Length)) > buffer.Length) + Array.Resize (ref buffer, (int) nread); + + // read the certificate data + return (int) reader.GetBytes (column, 0, buffer, 0, (int) nread); +#endif + } + + static X509Certificate DecodeCertificate (DbDataReader reader, X509CertificateParser parser, int column, ref byte[] buffer) + { + int nread = ReadBinaryBlob (reader, column, ref buffer); + + using (var memory = new MemoryStream (buffer, 0, nread, false)) { + return parser.ReadCertificate (memory); + } + } + + static X509Crl DecodeX509Crl (DbDataReader reader, X509CrlParser parser, int column, ref byte[] buffer) + { + int nread = ReadBinaryBlob (reader, column, ref buffer); + + using (var memory = new MemoryStream (buffer, 0, nread, false)) { + return parser.ReadCrl (memory); + } + } + + byte[] EncryptAsymmetricKeyParameter (AsymmetricKeyParameter key) + { + var cipher = PbeUtilities.CreateEngine (EncryptionAlgorithm.Id) as IBufferedCipher; + var keyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo (key); + var random = new SecureRandom (); + var salt = new byte[SaltSize]; + + if (cipher == null) + throw new Exception ("Unknown encryption algorithm: " + EncryptionAlgorithm.Id); + + random.NextBytes (salt); + + var pbeParameters = PbeUtilities.GenerateAlgorithmParameters (EncryptionAlgorithm.Id, salt, MinIterations); + var algorithm = new AlgorithmIdentifier (EncryptionAlgorithm, pbeParameters); + var cipherParameters = PbeUtilities.GenerateCipherParameters (algorithm, passwd); + + if (cipherParameters == null) + throw new Exception ("BouncyCastle bug detected: Failed to generate cipher parameters."); + + cipher.Init (true, cipherParameters); + + var encoded = cipher.DoFinal (keyInfo.GetEncoded ()); + + var encrypted = new EncryptedPrivateKeyInfo (algorithm, encoded); + + return encrypted.GetEncoded (); + } + + AsymmetricKeyParameter DecryptAsymmetricKeyParameter (byte[] buffer, int length) + { + using (var memory = new MemoryStream (buffer, 0, length, false)) { + using (var asn1 = new Asn1InputStream (memory)) { + var sequence = asn1.ReadObject () as Asn1Sequence; + if (sequence == null) + return null; + + var encrypted = EncryptedPrivateKeyInfo.GetInstance (sequence); + var algorithm = encrypted.EncryptionAlgorithm; + var encoded = encrypted.GetEncryptedData (); + + var cipher = PbeUtilities.CreateEngine (algorithm) as IBufferedCipher; + if (cipher == null) + return null; + + var cipherParameters = PbeUtilities.GenerateCipherParameters (algorithm, passwd); + + if (cipherParameters == null) + throw new Exception ("BouncyCastle bug detected: Failed to generate cipher parameters."); + + cipher.Init (false, cipherParameters); + + var decrypted = cipher.DoFinal (encoded); + var keyInfo = PrivateKeyInfo.GetInstance (decrypted); + + return PrivateKeyFactory.CreateKey (keyInfo); + } + } + } + + AsymmetricKeyParameter DecodePrivateKey (DbDataReader reader, int column, ref byte[] buffer) + { + if (reader.IsDBNull (column)) + return null; + + int nread = ReadBinaryBlob (reader, column, ref buffer); + + return DecryptAsymmetricKeyParameter (buffer, nread); + } + + object EncodePrivateKey (AsymmetricKeyParameter key) + { + return key != null ? (object) EncryptAsymmetricKeyParameter (key) : DBNull.Value; + } + + static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (DbDataReader reader, int column) + { + if (reader.IsDBNull (column)) + return null; + + var algorithms = new List (); + var values = reader.GetString (column); + + foreach (var token in values.Split (new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { + EncryptionAlgorithm algorithm; + + if (Enum.TryParse (token.Trim (), true, out algorithm)) + algorithms.Add (algorithm); + } + + return algorithms.ToArray (); + } + + static object EncodeEncryptionAlgorithms (EncryptionAlgorithm[] algorithms) + { + if (algorithms == null || algorithms.Length == 0) + return DBNull.Value; + + var tokens = new string[algorithms.Length]; + for (int i = 0; i < algorithms.Length; i++) + tokens[i] = algorithms[i].ToString (); + + return string.Join (",", tokens); + } + + X509CertificateRecord LoadCertificateRecord (DbDataReader reader, X509CertificateParser parser, ref byte[] buffer) + { + var record = new X509CertificateRecord (); + + for (int i = 0; i < reader.FieldCount; i++) { + switch (reader.GetName (i).ToUpperInvariant ()) { + case "CERTIFICATE": + record.Certificate = DecodeCertificate (reader, parser, i, ref buffer); + break; + case "PRIVATEKEY": + record.PrivateKey = DecodePrivateKey (reader, i, ref buffer); + break; + case "ALGORITHMS": + record.Algorithms = DecodeEncryptionAlgorithms (reader, i); + break; + case "ALGORITHMSUPDATED": + record.AlgorithmsUpdated = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); + break; + case "TRUSTED": + record.IsTrusted = reader.GetBoolean (i); + break; + case "ID": + record.Id = reader.GetInt32 (i); + break; + } + } + + return record; + } + + X509CrlRecord LoadCrlRecord (DbDataReader reader, X509CrlParser parser, ref byte[] buffer) + { + var record = new X509CrlRecord (); + + for (int i = 0; i < reader.FieldCount; i++) { + switch (reader.GetName (i).ToUpperInvariant ()) { + case "CRL": + record.Crl = DecodeX509Crl (reader, parser, i, ref buffer); + break; + case "THISUPDATE": + record.ThisUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); + break; + case "NEXTUPDATE": + record.NextUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); + break; + case "DELTA": + record.IsDelta = reader.GetBoolean (i); + break; + case "ID": + record.Id = reader.GetInt32 (i); + break; + } + } + + return record; + } + + /// + /// Gets the column names for the specified fields. + /// + /// + /// Gets the column names for the specified fields. + /// + /// The column names. + /// The fields. + protected static string[] GetColumnNames (X509CertificateRecordFields fields) + { + var columns = new List (); + + if ((fields & X509CertificateRecordFields.Id) != 0) + columns.Add ("ID"); + if ((fields & X509CertificateRecordFields.Trusted) != 0) + columns.Add ("TRUSTED"); + if ((fields & X509CertificateRecordFields.Algorithms) != 0) + columns.Add ("ALGORITHMS"); + if ((fields & X509CertificateRecordFields.AlgorithmsUpdated) != 0) + columns.Add ("ALGORITHMSUPDATED"); + if ((fields & X509CertificateRecordFields.Certificate) != 0) + columns.Add ("CERTIFICATE"); + if ((fields & X509CertificateRecordFields.PrivateKey) != 0) + columns.Add ("PRIVATEKEY"); + + return columns.ToArray (); + } + + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// + /// Gets the database command to select the record matching the specified certificate. + /// + /// The database command. + /// The certificate. + /// The fields to return. + protected abstract DbCommand GetSelectCommand (X509Certificate certificate, X509CertificateRecordFields fields); + + /// + /// Gets the database command to select the certificate records for the specified mailbox. + /// + /// + /// Gets the database command to select the certificate records for the specified mailbox. + /// + /// The database command. + /// The mailbox. + /// The date and time for which the certificate should be valid. + /// true if the certificate must have a private key; otherwise, false. + /// The fields to return. + protected abstract DbCommand GetSelectCommand (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields); + + /// + /// Gets the database command to select certificate records matching the specified selector. + /// + /// + /// Gets the database command to select certificate records matching the specified selector. + /// + /// The database command. + /// The certificate selector. + /// true if only trusted anchor certificates should be matched; otherwise, false. + /// true if the certificate must have a private key; otherwise, false. + /// The fields to return. + protected abstract DbCommand GetSelectCommand (IX509Selector selector, bool trustedAnchorsOnly, bool requirePrivateKey, X509CertificateRecordFields fields); + + /// + /// Gets the column names for the specified fields. + /// + /// + /// Gets the column names for the specified fields. + /// + /// The column names. + /// The fields. + protected static string[] GetColumnNames (X509CrlRecordFields fields) + { + var columns = new List (); + + if ((fields & X509CrlRecordFields.Id) != 0) + columns.Add ("ID"); + if ((fields & X509CrlRecordFields.IsDelta) != 0) + columns.Add ("DELTA"); + if ((fields & X509CrlRecordFields.IssuerName) != 0) + columns.Add ("ISSUERNAME"); + if ((fields & X509CrlRecordFields.ThisUpdate) != 0) + columns.Add ("THISUPDATE"); + if ((fields & X509CrlRecordFields.NextUpdate) != 0) + columns.Add ("NEXTUPDATE"); + if ((fields & X509CrlRecordFields.Crl) != 0) + columns.Add ("CRL"); + + return columns.ToArray (); + } + + /// + /// Gets the database command to select the CRL records matching the specified issuer. + /// + /// + /// Gets the database command to select the CRL records matching the specified issuer. + /// + /// The database command. + /// The issuer. + /// The fields to return. + protected abstract DbCommand GetSelectCommand (X509Name issuer, X509CrlRecordFields fields); + + /// + /// Gets the database command to select the record for the specified CRL. + /// + /// + /// Gets the database command to select the record for the specified CRL. + /// + /// The database command. + /// The X.509 CRL. + /// The fields to return. + protected abstract DbCommand GetSelectCommand (X509Crl crl, X509CrlRecordFields fields); + + /// + /// Gets the database command to select all CRLs in the table. + /// + /// + /// Gets the database command to select all CRLs in the table. + /// + /// The database command. + protected abstract DbCommand GetSelectAllCrlsCommand (); + + /// + /// Gets the database command to delete the specified certificate record. + /// + /// + /// Gets the database command to delete the specified certificate record. + /// + /// The database command. + /// The certificate record. + protected abstract DbCommand GetDeleteCommand (X509CertificateRecord record); + + /// + /// Gets the database command to delete the specified CRL record. + /// + /// + /// Gets the database command to delete the specified CRL record. + /// + /// The database command. + /// The record. + protected abstract DbCommand GetDeleteCommand (X509CrlRecord record); + + /// + /// Gets the value for the specified column. + /// + /// + /// Gets the value for the specified column. + /// + /// The value. + /// The certificate record. + /// The column name. + /// + /// is not a known column name. + /// + protected object GetValue (X509CertificateRecord record, string columnName) + { + switch (columnName) { + //case "ID": return record.Id; + case "BASICCONSTRAINTS": return record.BasicConstraints; + case "TRUSTED": return record.IsTrusted; + case "ANCHOR": return record.IsAnchor; + case "KEYUSAGE": return (int) record.KeyUsage; + case "NOTBEFORE": return record.NotBefore.ToUniversalTime (); + case "NOTAFTER": return record.NotAfter.ToUniversalTime (); + case "ISSUERNAME": return record.IssuerName; + case "SERIALNUMBER": return record.SerialNumber; + case "SUBJECTNAME": return record.SubjectName; + case "SUBJECTKEYIDENTIFIER": return record.SubjectKeyIdentifier?.AsHex (); + case "SUBJECTEMAIL": return record.SubjectEmail != null ? record.SubjectEmail.ToLowerInvariant () : string.Empty; + case "FINGERPRINT": return record.Fingerprint.ToLowerInvariant (); + case "ALGORITHMS": return EncodeEncryptionAlgorithms (record.Algorithms); + case "ALGORITHMSUPDATED": return record.AlgorithmsUpdated; + case "CERTIFICATE": return record.Certificate.GetEncoded (); + case "PRIVATEKEY": return EncodePrivateKey (record.PrivateKey); + default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName)); + } + } + + /// + /// Gets the value for the specified column. + /// + /// + /// Gets the value for the specified column. + /// + /// The value. + /// The CRL record. + /// The column name. + /// + /// is not a known column name. + /// + protected static object GetValue (X509CrlRecord record, string columnName) + { + switch (columnName) { + //case "ID": return record.Id; + case "DELTA": return record.IsDelta; + case "ISSUERNAME": return record.IssuerName; + case "THISUPDATE": return record.ThisUpdate; + case "NEXTUPDATE": return record.NextUpdate; + case "CRL": return record.Crl.GetEncoded (); + default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName)); + } + } + + /// + /// Gets the database command to insert the specified certificate record. + /// + /// + /// Gets the database command to insert the specified certificate record. + /// + /// The database command. + /// The certificate record. + protected abstract DbCommand GetInsertCommand (X509CertificateRecord record); + + /// + /// Gets the database command to insert the specified CRL record. + /// + /// + /// Gets the database command to insert the specified CRL record. + /// + /// The database command. + /// The CRL record. + protected abstract DbCommand GetInsertCommand (X509CrlRecord record); + + /// + /// Gets the database command to update the specified record. + /// + /// + /// Gets the database command to update the specified record. + /// + /// The database command. + /// The certificate record. + /// The fields to update. + protected abstract DbCommand GetUpdateCommand (X509CertificateRecord record, X509CertificateRecordFields fields); + + /// + /// Gets the database command to update the specified CRL record. + /// + /// + /// Gets the database command to update the specified CRL record. + /// + /// The database command. + /// The CRL record. + protected abstract DbCommand GetUpdateCommand (X509CrlRecord record); + + /// + /// Find the specified certificate. + /// + /// + /// Searches the database for the specified certificate, returning the matching + /// record with the desired fields populated. + /// + /// The matching record if found; otherwise null. + /// The certificate. + /// The desired fields. + /// + /// is null. + /// + public X509CertificateRecord Find (X509Certificate certificate, X509CertificateRecordFields fields) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + using (var command = GetSelectCommand (certificate, fields)) { + using (var reader = command.ExecuteReader ()) { + if (reader.Read ()) { + var parser = new X509CertificateParser (); + var buffer = new byte[4096]; + + return LoadCertificateRecord (reader, parser, ref buffer); + } + } + } + + return null; + } + + /// + /// Finds the certificates matching the specified selector. + /// + /// + /// Searches the database for certificates matching the selector, returning all + /// matching certificates. + /// + /// The matching certificates. + /// The match selector or null to return all certificates. + public IEnumerable FindCertificates (IX509Selector selector) + { + using (var command = GetSelectCommand (selector, false, false, X509CertificateRecordFields.Certificate)) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CertificateParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + var record = LoadCertificateRecord (reader, parser, ref buffer); + if (selector == null || selector.Match (record.Certificate)) + yield return record.Certificate; + } + } + } + + yield break; + } + + /// + /// Finds the private keys matching the specified selector. + /// + /// + /// Searches the database for certificate records matching the selector, returning the + /// private keys for each matching record. + /// + /// The matching certificates. + /// The match selector or null to return all private keys. + public IEnumerable FindPrivateKeys (IX509Selector selector) + { + using (var command = GetSelectCommand (selector, false, true, PrivateKeyFields)) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CertificateParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + var record = LoadCertificateRecord (reader, parser, ref buffer); + + if (selector == null || selector.Match (record.Certificate)) + yield return record.PrivateKey; + } + } + } + + yield break; + } + + /// + /// Finds the certificate records for the specified mailbox. + /// + /// + /// Searches the database for certificates matching the specified mailbox that are valid + /// for the date and time specified, returning all matching records populated with the + /// desired fields. + /// + /// The matching certificate records populated with the desired fields. + /// The mailbox. + /// The date and time. + /// true if a private key is required. + /// The desired fields. + /// + /// is null. + /// + public IEnumerable Find (MailboxAddress mailbox, DateTime now, bool requirePrivateKey, X509CertificateRecordFields fields) + { + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + using (var command = GetSelectCommand (mailbox, now, requirePrivateKey, fields)) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CertificateParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + yield return LoadCertificateRecord (reader, parser, ref buffer); + } + } + } + + yield break; + } + + /// + /// Finds the certificate records matching the specified selector. + /// + /// + /// Searches the database for certificate records matching the selector, returning all + /// of the matching records populated with the desired fields. + /// + /// The matching certificate records populated with the desired fields. + /// The match selector or null to match all certificates. + /// true if only trusted anchor certificates should be returned. + /// The desired fields. + public IEnumerable Find (IX509Selector selector, bool trustedAnchorsOnly, X509CertificateRecordFields fields) + { + using (var command = GetSelectCommand (selector, trustedAnchorsOnly, false, fields | X509CertificateRecordFields.Certificate)) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CertificateParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + var record = LoadCertificateRecord (reader, parser, ref buffer); + + if (selector == null || selector.Match (record.Certificate)) + yield return record; + } + } + } + + yield break; + } + + /// + /// Add the specified certificate record. + /// + /// + /// Adds the specified certificate record to the database. + /// + /// The certificate record. + /// + /// is null. + /// + public void Add (X509CertificateRecord record) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetInsertCommand (record)) + command.ExecuteNonQuery (); + } + + /// + /// Remove the specified certificate record. + /// + /// + /// Removes the specified certificate record from the database. + /// + /// The certificate record. + /// + /// is null. + /// + public void Remove (X509CertificateRecord record) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetDeleteCommand (record)) + command.ExecuteNonQuery (); + } + + /// + /// Update the specified certificate record. + /// + /// + /// Updates the specified fields of the record in the database. + /// + /// The certificate record. + /// The fields to update. + /// + /// is null. + /// + public void Update (X509CertificateRecord record, X509CertificateRecordFields fields) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetUpdateCommand (record, fields)) + command.ExecuteNonQuery (); + } + + /// + /// Finds the CRL records for the specified issuer. + /// + /// + /// Searches the database for CRL records matching the specified issuer, returning + /// all matching records populated with the desired fields. + /// + /// The matching CRL records populated with the desired fields. + /// The issuer. + /// The desired fields. + /// + /// is null. + /// + public IEnumerable Find (X509Name issuer, X509CrlRecordFields fields) + { + if (issuer == null) + throw new ArgumentNullException (nameof (issuer)); + + using (var command = GetSelectCommand (issuer, fields)) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CrlParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + yield return LoadCrlRecord (reader, parser, ref buffer); + } + } + } + + yield break; + } + + /// + /// Finds the specified certificate revocation list. + /// + /// + /// Searches the database for the specified CRL, returning the matching record with + /// the desired fields populated. + /// + /// The matching record if found; otherwise null. + /// The certificate revocation list. + /// The desired fields. + /// + /// is null. + /// + public X509CrlRecord Find (X509Crl crl, X509CrlRecordFields fields) + { + if (crl == null) + throw new ArgumentNullException (nameof (crl)); + + using (var command = GetSelectCommand (crl, fields)) { + using (var reader = command.ExecuteReader ()) { + if (reader.Read ()) { + var parser = new X509CrlParser (); + var buffer = new byte[4096]; + + return LoadCrlRecord (reader, parser, ref buffer); + } + } + } + + return null; + } + + /// + /// Add the specified CRL record. + /// + /// + /// Adds the specified CRL record to the database. + /// + /// The CRL record. + /// + /// is null. + /// + public void Add (X509CrlRecord record) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetInsertCommand (record)) + command.ExecuteNonQuery (); + } + + /// + /// Remove the specified CRL record. + /// + /// + /// Removes the specified CRL record from the database. + /// + /// The CRL record. + /// + /// is null. + /// + public void Remove (X509CrlRecord record) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetDeleteCommand (record)) + command.ExecuteNonQuery (); + } + + /// + /// Update the specified CRL record. + /// + /// + /// Updates the specified fields of the record in the database. + /// + /// The CRL record. + /// + /// is null. + /// + public void Update (X509CrlRecord record) + { + if (record == null) + throw new ArgumentNullException (nameof (record)); + + using (var command = GetUpdateCommand (record)) + command.ExecuteNonQuery (); + } + + /// + /// Gets a certificate revocation list store. + /// + /// + /// Gets a certificate revocation list store. + /// + /// A certificate revocation list store. + public IX509Store GetCrlStore () + { + var crls = new List (); + + using (var command = GetSelectAllCrlsCommand ()) { + using (var reader = command.ExecuteReader ()) { + var parser = new X509CrlParser (); + var buffer = new byte[4096]; + + while (reader.Read ()) { + var record = LoadCrlRecord (reader, parser, ref buffer); + crls.Add (record.Crl); + } + } + } + + return X509StoreFactory.Create ("Crl/Collection", new X509CollectionStoreParameters (crls)); + } + +#region IX509Store implementation + + /// + /// Gets a collection of matching certificates matching the specified selector. + /// + /// + /// Gets a collection of matching certificates matching the specified selector. + /// + /// The matching certificates. + /// The match criteria. + ICollection IX509Store.GetMatches (IX509Selector selector) + { + return new List (FindCertificates (selector)); + } + +#endregion + +#region IDisposable implementation + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected virtual void Dispose (bool disposing) + { + if (passwd != null) { + for (int i = 0; i < passwd.Length; i++) + passwd[i] = '\0'; + } + } + + /// + /// Releases all resource used by the object. + /// + /// Call when you are finished using the + /// . The method leaves the + /// in an unusable state. After calling + /// , you must release all references to the + /// so the garbage collector can reclaim the memory that + /// the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + +#endregion + } +} diff --git a/src/MimeKit/Cryptography/X509CertificateRecord.cs b/src/MimeKit/Cryptography/X509CertificateRecord.cs new file mode 100644 index 0000000..fa971db --- /dev/null +++ b/src/MimeKit/Cryptography/X509CertificateRecord.cs @@ -0,0 +1,315 @@ +// +// X509CertificateRecord.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Asn1.X509; + +namespace MimeKit.Cryptography { + /// + /// X.509 certificate record fields. + /// + /// + /// The record fields are used when querying the + /// for certificates. + /// + [Flags] + public enum X509CertificateRecordFields { + /// + /// The "id" field is typically just the ROWID in the database. + /// + Id = 1 << 0, + + /// + /// The "trusted" field is a boolean value indicating whether the certificate + /// is trusted. + /// + Trusted = 1 << 1, + + /// + /// The "algorithms" field is used for storing the last known list of + /// values that are supported by the + /// client associated with the certificate. + /// + Algorithms = 1 << 3, + + /// + /// The "algorithms updated" field is used to store the timestamp of the + /// most recent update to the Algorithms field. + /// + AlgorithmsUpdated = 1 << 4, + + /// + /// The "certificate" field is sued for storing the binary data of the actual + /// certificate. + /// + Certificate = 1 << 5, + + /// + /// The "private key" field is used to store the encrypted binary data of the + /// private key associated with the certificate, if available. + /// + PrivateKey = 1 << 6, + } + + /// + /// An X.509 certificate record. + /// + /// + /// Represents an X.509 certificate record loaded from a database. + /// + public class X509CertificateRecord + { + /// + /// Gets the identifier. + /// + /// + /// The id is typically the ROWID of the certificate in the database and is not + /// generally useful outside of the internals of the database implementation. + /// + /// The identifier. + public int Id { get; internal set; } + + /// + /// Gets the basic constraints of the certificate. + /// + /// + /// Gets the basic constraints of the certificate. + /// + /// The basic constraints of the certificate. + public int BasicConstraints { get { return Certificate.GetBasicConstraints (); } } + + /// + /// Gets or sets a value indicating whether the certificate is trusted. + /// + /// + /// Indiciates whether or not the certificate is trusted. + /// + /// true if the certificate is trusted; otherwise, false. + public bool IsTrusted { get; set; } + + /// + /// Gets whether or not the certificate is an anchor. + /// + /// + /// Gets whether or not the certificate is an anchor. + /// + /// true if the certificate is an anchor; otherwise, false. + public bool IsAnchor { get { return Certificate.IsSelfSigned (); } } + + /// + /// Gets the key usage flags for the certificate. + /// + /// + /// Gets the key usage flags for the certificate. + /// + /// The X.509 key usage. + public X509KeyUsageFlags KeyUsage { get { return Certificate.GetKeyUsageFlags (); } } + + /// + /// Gets the starting date and time where the certificate is valid. + /// + /// + /// Gets the starting date and time where the certificate is valid. + /// + /// The date and time in coordinated universal time (UTC). + public DateTime NotBefore { get { return Certificate.NotBefore.ToUniversalTime (); } } + + /// + /// Gets the end date and time where the certificate is valid. + /// + /// + /// Gets the end date and time where the certificate is valid. + /// + /// The date and time in coordinated universal time (UTC). + public DateTime NotAfter { get { return Certificate.NotAfter.ToUniversalTime (); } } + + /// + /// Gets the certificate's issuer name. + /// + /// + /// Gets the certificate's issuer name. + /// + /// The certificate's issuer name. + public string IssuerName { get { return Certificate.IssuerDN.ToString (); } } + + /// + /// Gets the serial number of the certificate. + /// + /// + /// Gets the serial number of the certificate. + /// + /// The serial number. + public string SerialNumber { get { return Certificate.SerialNumber.ToString (); } } + + /// + /// Gets the certificate's subject name. + /// + /// + /// Gets the certificate's subject name. + /// + /// The certificate's subject name. + public string SubjectName { get { return Certificate.SubjectDN.ToString (); } } + + /// + /// Gets the certificate's subject key identifier. + /// + /// + /// Gets the certificate's subject key identifier. + /// + /// The certificate's subject key identifier. + public byte[] SubjectKeyIdentifier { + get { + var subjectKeyIdentifier = Certificate.GetExtensionValue (X509Extensions.SubjectKeyIdentifier); + + if (subjectKeyIdentifier != null) + subjectKeyIdentifier = (Asn1OctetString) Asn1Object.FromByteArray (subjectKeyIdentifier.GetOctets ()); + + return subjectKeyIdentifier?.GetOctets (); + } + } + + /// + /// Gets the subject email address. + /// + /// + /// Gets the subject email address. + /// + /// The subject email address. + public string SubjectEmail { get { return Certificate.GetSubjectEmailAddress (); } } + + /// + /// Gets the fingerprint of the certificate. + /// + /// + /// Gets the fingerprint of the certificate. + /// + /// The fingerprint. + public string Fingerprint { get { return Certificate.GetFingerprint (); } } + + /// + /// Gets or sets the encryption algorithm capabilities. + /// + /// + /// Gets or sets the encryption algorithm capabilities. + /// + /// The encryption algorithms. + public EncryptionAlgorithm[] Algorithms { get; set; } + + /// + /// Gets or sets the date when the algorithms were last updated. + /// + /// + /// Gets or sets the date when the algorithms were last updated. + /// + /// The date the algorithms were updated. + public DateTime AlgorithmsUpdated { get; set; } + + /// + /// Gets the certificate. + /// + /// + /// Gets the certificate. + /// + /// The certificate. + public X509Certificate Certificate { get; internal set; } + + /// + /// Gets the private key. + /// + /// + /// Gets the private key. + /// + /// The private key. + public AsymmetricKeyParameter PrivateKey { get; set; } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new certificate record with a private key for storing in a + /// . + /// + /// The certificate. + /// The private key. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a private key. + /// + public X509CertificateRecord (X509Certificate certificate, AsymmetricKeyParameter key) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (key == null) + throw new ArgumentNullException (nameof (key)); + + if (!key.IsPrivate) + throw new ArgumentException ("The key must be private.", nameof (key)); + + AlgorithmsUpdated = DateTime.MinValue; + Certificate = certificate; + PrivateKey = key; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new certificate record for storing in a . + /// + /// The certificate. + /// + /// is null. + /// + public X509CertificateRecord (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + AlgorithmsUpdated = DateTime.MinValue; + Certificate = certificate; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is only meant to be used by implementors of + /// when loading records from the database. + /// + public X509CertificateRecord () + { + } + } +} diff --git a/src/MimeKit/Cryptography/X509CertificateStore.cs b/src/MimeKit/Cryptography/X509CertificateStore.cs new file mode 100644 index 0000000..6bdd6e0 --- /dev/null +++ b/src/MimeKit/Cryptography/X509CertificateStore.cs @@ -0,0 +1,544 @@ +// +// X509CertificateStore.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; + +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Store; + +namespace MimeKit.Cryptography { + /// + /// A store for X.509 certificates and keys. + /// + /// + /// A store for X.509 certificates and keys. + /// + public class X509CertificateStore : IX509Store + { + readonly Dictionary keys; + readonly HashSet unique; + readonly List certs; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public X509CertificateStore () + { + keys = new Dictionary (); + unique = new HashSet (); + certs = new List (); + } + + /// + /// Enumerates the certificates currently in the store. + /// + /// + /// Enumerates the certificates currently in the store. + /// + /// The certificates. + public IEnumerable Certificates { + get { return certs; } + } + + /// + /// Gets the private key for the specified certificate. + /// + /// + /// Gets the private key for the specified certificate, if it exists. + /// + /// The private key on success; otherwise null. + /// The certificate. + public AsymmetricKeyParameter GetPrivateKey (X509Certificate certificate) + { + AsymmetricKeyParameter key; + + if (!keys.TryGetValue (certificate, out key)) + return null; + + return key; + } + + /// + /// Adds the specified certificate to the store. + /// + /// + /// Adds the specified certificate to the store. + /// + /// The certificate. + /// + /// is null. + /// + public void Add (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (unique.Add (certificate)) + certs.Add (certificate); + } + + /// + /// Adds the specified range of certificates to the store. + /// + /// + /// Adds the specified range of certificates to the store. + /// + /// The certificates. + /// + /// is null. + /// + public void AddRange (IEnumerable certificates) + { + if (certificates == null) + throw new ArgumentNullException (nameof (certificates)); + + foreach (var certificate in certificates) { + if (unique.Add (certificate)) + certs.Add (certificate); + } + } + + /// + /// Removes the specified certificate from the store. + /// + /// + /// Removes the specified certificate from the store. + /// + /// The certificate. + /// + /// is null. + /// + public void Remove (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + if (unique.Remove (certificate)) + certs.Remove (certificate); + } + + /// + /// Removes the specified range of certificates from the store. + /// + /// + /// Removes the specified range of certificates from the store. + /// + /// The certificates. + /// + /// is null. + /// + public void RemoveRange (IEnumerable certificates) + { + if (certificates == null) + throw new ArgumentNullException (nameof (certificates)); + + foreach (var certificate in certificates) { + if (unique.Remove (certificate)) + certs.Remove (certificate); + } + } + + /// + /// Imports the certificate(s) from the specified stream. + /// + /// + /// Imports the certificate(s) from the specified stream. + /// + /// The stream to import. + /// + /// is null. + /// + /// + /// An error occurred reading the stream. + /// + public void Import (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new X509CertificateParser (); + + foreach (X509Certificate certificate in parser.ReadCertificates (stream)) { + if (unique.Add (certificate)) + certs.Add (certificate); + } + } + + /// + /// Imports the certificate(s) from the specified file. + /// + /// + /// Imports the certificate(s) from the specified file. + /// + /// The name of the file to import. + /// + /// is null. + /// + /// + /// The specified file could not be found. + /// + /// + /// An error occurred reading the file. + /// + public void Import (string fileName) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + Import (stream); + } + + /// + /// Imports the certificate(s) from the specified byte array. + /// + /// + /// Imports the certificate(s) from the specified byte array. + /// + /// The raw certificate data. + /// + /// is null. + /// + public void Import (byte[] rawData) + { + if (rawData == null) + throw new ArgumentNullException (nameof (rawData)); + + using (var stream = new MemoryStream (rawData, false)) + Import (stream); + } + + /// + /// Imports certificates and private keys from the specified stream. + /// + /// + /// Imports certificates and private keys from the specified pkcs12 stream. + /// + /// The stream to import. + /// The password to unlock the stream. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred reading the stream. + /// + public void Import (Stream stream, string password) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ()); + + foreach (string alias in pkcs12.Aliases) { + if (pkcs12.IsKeyEntry (alias)) { + var chain = pkcs12.GetCertificateChain (alias); + var entry = pkcs12.GetKey (alias); + + for (int i = 0; i < chain.Length; i++) { + if (unique.Add (chain[i].Certificate)) + certs.Add (chain[i].Certificate); + } + + if (entry.Key.IsPrivate) + keys.Add (chain[0].Certificate, entry.Key); + } else if (pkcs12.IsCertificateEntry (alias)) { + var entry = pkcs12.GetCertificate (alias); + + if (unique.Add (entry.Certificate)) + certs.Add (entry.Certificate); + } + } + } + + /// + /// Imports certificates and private keys from the specified file. + /// + /// + /// Imports certificates and private keys from the specified pkcs12 stream. + /// + /// The name of the file to import. + /// The password to unlock the file. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The specified file could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// An error occurred reading the file. + /// + public void Import (string fileName, string password) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + Import (stream, password); + } + + /// + /// Imports certificates and private keys from the specified byte array. + /// + /// + /// Imports certificates and private keys from the specified pkcs12 stream. + /// + /// The raw certificate data. + /// The password to unlock the raw data. + /// + /// is null. + /// -or- + /// is null. + /// + public void Import (byte[] rawData, string password) + { + if (rawData == null) + throw new ArgumentNullException (nameof (rawData)); + + using (var stream = new MemoryStream (rawData, false)) + Import (stream, password); + } + + /// + /// Exports the certificates to an unencrypted stream. + /// + /// + /// Exports the certificates to an unencrypted stream. + /// + /// The output stream. + /// + /// is null. + /// + /// + /// An error occurred while writing to the stream. + /// + public void Export (Stream stream) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + foreach (var certificate in certs) { + var encoded = certificate.GetEncoded (); + stream.Write (encoded, 0, encoded.Length); + } + } + + /// + /// Exports the certificates to an unencrypted file. + /// + /// + /// Exports the certificates to an unencrypted file. + /// + /// The file path to write to. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The specified path exceeds the maximum allowed path length of the system. + /// + /// + /// A directory in the specified path does not exist. + /// + /// + /// The user does not have access to create the specified file. + /// + /// + /// An error occurred while writing to the stream. + /// + public void Export (string fileName) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var file = File.Create (fileName)) + Export (file); + } + + /// + /// Exports the specified stream and password to a pkcs12 encrypted file. + /// + /// + /// Exports the specified stream and password to a pkcs12 encrypted file. + /// + /// The output stream. + /// The password to use to lock the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// An error occurred while writing to the stream. + /// + public void Export (Stream stream, string password) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + var store = new Pkcs12Store (); + foreach (var certificate in certs) { + if (keys.ContainsKey (certificate)) + continue; + + var alias = certificate.GetCommonName (); + + if (alias == null) + continue; + + var entry = new X509CertificateEntry (certificate); + + store.SetCertificateEntry (alias, entry); + } + + foreach (var kvp in keys) { + var alias = kvp.Key.GetCommonName (); + + if (alias == null) + continue; + + var entry = new AsymmetricKeyEntry (kvp.Value); + var cert = new X509CertificateEntry (kvp.Key); + var chain = new List (); + + chain.Add (cert); + + store.SetKeyEntry (alias, entry, chain.ToArray ()); + } + + store.Save (stream, password.ToCharArray (), new SecureRandom ()); + } + + /// + /// Exports the specified stream and password to a pkcs12 encrypted file. + /// + /// + /// Exports the specified stream and password to a pkcs12 encrypted file. + /// + /// The file path to write to. + /// The password to use to lock the private keys. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The specified path exceeds the maximum allowed path length of the system. + /// + /// + /// A directory in the specified path does not exist. + /// + /// + /// The user does not have access to create the specified file. + /// + /// + /// An error occurred while writing to the stream. + /// + public void Export (string fileName, string password) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + if (password == null) + throw new ArgumentNullException (nameof (password)); + + using (var file = File.Create (fileName)) + Export (file, password); + } + + /// + /// Gets an enumerator of matching X.509 certificates based on the specified selector. + /// + /// + /// Gets an enumerator of matching X.509 certificates based on the specified selector. + /// + /// The matching certificates. + /// The match criteria. + public IEnumerable GetMatches (IX509Selector selector) + { + foreach (var certificate in certs) { + if (selector == null || selector.Match (certificate)) + yield return certificate; + } + + yield break; + } + + #region IX509Store implementation + + /// + /// Gets a collection of matching X.509 certificates based on the specified selector. + /// + /// + /// Gets a collection of matching X.509 certificates based on the specified selector. + /// + /// The matching certificates. + /// The match criteria. + ICollection IX509Store.GetMatches (IX509Selector selector) + { + var matches = new List (); + + foreach (var certificate in GetMatches (selector)) + matches.Add (certificate); + + return matches; + } + + #endregion + } +} diff --git a/src/MimeKit/Cryptography/X509CrlRecord.cs b/src/MimeKit/Cryptography/X509CrlRecord.cs new file mode 100644 index 0000000..1bb17b6 --- /dev/null +++ b/src/MimeKit/Cryptography/X509CrlRecord.cs @@ -0,0 +1,172 @@ +// +// X509CrlRecord.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using Org.BouncyCastle.X509; + +namespace MimeKit.Cryptography { + /// + /// X.509 certificate revocation list record fields. + /// + /// + /// The record fields are used when querying the + /// for certificate revocation lists. + /// + [Flags] + public enum X509CrlRecordFields { + /// + /// The "id" field is typically just the ROWID in the database. + /// + Id = 1 << 0, + + /// + /// The "delta" field is a boolean value indicating whether the certificate + /// revocation list is a delta. + /// + IsDelta = 1 << 1, + + /// + /// The "issuer name" field stores the issuer name of the certificate revocation list. + /// + IssuerName = 1 << 2, + + /// + /// The "this update" field stores the date and time of the most recent update. + /// + ThisUpdate = 1 << 3, + + /// + /// The "next update" field stores the date and time of the next scheduled update. + /// + NextUpdate = 1 << 4, + + /// + /// The "crl" field stores the raw binary data of the certificate revocation list. + /// + Crl = 1 << 5, + } + + /// + /// An X.509 certificate revocation list (CRL) record. + /// + /// + /// Represents an X.509 certificate revocation list record loaded from a database. + /// + public class X509CrlRecord + { + /// + /// Gets the identifier. + /// + /// + /// The id is typically the ROWID of the certificate revocation list in the + /// database and is not generally useful outside of the internals of the + /// database implementation. + /// + /// The identifier. + public int Id { get; internal set; } + + /// + /// Gets whether or not this certificate revocation list is a delta. + /// + /// + /// Indicates whether or not this certificate revocation list is a delta. + /// + /// true if th crl is delta; otherwise, false. + public bool IsDelta { get; internal set; } + + /// + /// Gets the issuer name of the certificate revocation list. + /// + /// + /// Gets the issuer name of the certificate revocation list. + /// + /// The issuer's name. + public string IssuerName { get; internal set; } + + /// + /// Gets the date and time of the most recent update. + /// + /// + /// Gets the date and time of the most recent update. + /// + /// The date and time. + public DateTime ThisUpdate { get; internal set; } + + /// + /// Gets the date and time when the next CRL update will be published. + /// + /// + /// Gets the date and time when the next CRL update will be published. + /// + /// The date and time. + public DateTime NextUpdate { get; internal set; } + + /// + /// Gets the certificate revocation list. + /// + /// + /// Gets the certificate revocation list. + /// + /// The certificate revocation list. + public X509Crl Crl { get; set; } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new CRL record for storing in a . + /// + /// The certificate revocation list. + /// + /// is null. + /// + public X509CrlRecord (X509Crl crl) + { + if (crl == null) + throw new ArgumentNullException (nameof (crl)); + + if (crl.NextUpdate != null) + NextUpdate = crl.NextUpdate.Value.ToUniversalTime (); + + IssuerName = crl.IssuerDN.ToString (); + ThisUpdate = crl.ThisUpdate.ToUniversalTime (); + IsDelta = crl.IsDelta (); + Crl = crl; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is only meant to be used by implementors of + /// when loading records from the database. + /// + public X509CrlRecord () + { + } + } +} diff --git a/src/MimeKit/Cryptography/X509KeyUsageFlags.cs b/src/MimeKit/Cryptography/X509KeyUsageFlags.cs new file mode 100644 index 0000000..689924a --- /dev/null +++ b/src/MimeKit/Cryptography/X509KeyUsageFlags.cs @@ -0,0 +1,121 @@ +// +// X509KeyUsageFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Cryptography { + /// + /// X.509 key usage flags. + /// + /// + /// The X.509 Key Usage Flags can be used to determine what operations + /// a certificate can be used for. + /// A value of indicates that + /// there are no restrictions on the use of the + /// . + /// + [Flags] + public enum X509KeyUsageFlags { + /// + /// No limitations for the key usage are set. + /// + /// + /// The key may be used for anything. + /// + None = 0, + + /// + /// The key may only be used for enciphering data during key agreement. + /// + /// + /// When both the bit and the + /// bit are both set, the key + /// may be used only for enciphering data while + /// performing key agreement. + /// + EncipherOnly = 1 << 0, + + /// + /// The key may be used for verifying signatures on + /// certificate revocation lists (CRLs). + /// + CrlSign = 1 << 1, + + /// + /// The key may be used for verifying signatures on certificates. + /// + KeyCertSign = 1 << 2, + + /// + /// The key is meant to be used for key agreement. + /// + KeyAgreement = 1 << 3, + + /// + /// The key may be used for data encipherment. + /// + DataEncipherment = 1 << 4, + + /// + /// The key is meant to be used for key encipherment. + /// + KeyEncipherment = 1 << 5, + + /// + /// The key may be used to verify digital signatures used to + /// provide a non-repudiation service. + /// + NonRepudiation = 1 << 6, + + /// + /// The key may be used for digitally signing data. + /// + DigitalSignature = 1 << 7, + + /// + /// The key may only be used for deciphering data during key agreement. + /// + /// + /// When both the bit and the + /// bit are both set, the key + /// may be used only for deciphering data while + /// performing key agreement. + /// + DecipherOnly = 1 << 15 + } + + enum X509KeyUsageBits { + DigitalSignature, + NonRepudiation, + KeyEncipherment, + DataEncipherment, + KeyAgreement, + KeyCertSign, + CrlSign, + EncipherOnly, + DecipherOnly, + } +} diff --git a/src/MimeKit/DomainList.cs b/src/MimeKit/DomainList.cs new file mode 100644 index 0000000..7789607 --- /dev/null +++ b/src/MimeKit/DomainList.cs @@ -0,0 +1,477 @@ +// +// DomainList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A domain list. + /// + /// + /// Represents a list of domains, such as those that an email was routed through. + /// + public class DomainList : IList + { + readonly static byte[] DomainSentinels = new [] { (byte) ',', (byte) ':' }; + readonly List domains; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new based on the domains provided. + /// + /// A domain list. + /// + /// is null. + /// + public DomainList (IEnumerable domains) + { + if (domains == null) + throw new ArgumentNullException (nameof (domains)); + + this.domains = new List (domains); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public DomainList () + { + domains = new List (); + } + + #region IList implementation + + /// + /// Gets the index of the requested domain, if it exists. + /// + /// + /// Finds the index of the specified domain, if it exists. + /// + /// The index of the requested domain; otherwise -1. + /// The domain. + /// + /// is null. + /// + public int IndexOf (string domain) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + return domains.IndexOf (domain); + } + + /// + /// Insert the domain at the specified index. + /// + /// + /// Inserts the domain at the specified index in the list. + /// + /// The index to insert the domain. + /// The domain to insert. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, string domain) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + domains.Insert (index, domain); + OnChanged (); + } + + /// + /// Removes the domain at the specified index. + /// + /// + /// Removes the domain at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + domains.RemoveAt (index); + OnChanged (); + } + + /// + /// Gets or sets the domain at the specified index. + /// + /// + /// Gets or sets the domain at the specified index. + /// + /// The domain at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public string this [int index] { + get { return domains[index]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (domains[index] == value) + return; + + domains[index] = value; + OnChanged (); + } + } + + #endregion + + #region ICollection implementation + + /// + /// Add the specified domain. + /// + /// + /// Adds the specified domain to the end of the list. + /// + /// The domain. + /// + /// is null. + /// + public void Add (string domain) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + domains.Add (domain); + OnChanged (); + } + + /// + /// Clears the domain list. + /// + /// + /// Removes all of the domains in the list. + /// + public void Clear () + { + domains.Clear (); + OnChanged (); + } + + /// + /// Checks if the contains the specified domain. + /// + /// + /// Determines whether or not the domain list contains the specified domain. + /// + /// true if the specified domain is contained; + /// otherwise false. + /// The domain. + /// + /// is null. + /// + public bool Contains (string domain) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + return domains.Contains (domain); + } + + /// + /// Copies all of the domains in the to the specified array. + /// + /// + /// Copies all of the domains within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the domains to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (string[] array, int arrayIndex) + { + domains.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified domain. + /// + /// + /// Removes the first instance of the specified domain from the list if it exists. + /// + /// true if the domain was removed; otherwise false. + /// The domain. + /// + /// is null. + /// + public bool Remove (string domain) + { + if (domain == null) + throw new ArgumentNullException (nameof (domain)); + + if (domains.Remove (domain)) { + OnChanged (); + return true; + } + + return false; + } + + /// + /// Gets the number of domains in the . + /// + /// + /// Indicates the number of domains in the list. + /// + /// The number of domains. + public int Count { + get { return domains.Count; } + } + + /// + /// Get a value indicating whether the is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of domains. + /// + /// + /// Gets an enumerator for the list of domains. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return domains.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Gets an enumerator for the list of domains. + /// + /// + /// Gets an enumerator for the list of domains. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return domains.GetEnumerator (); + } + + #endregion + + static bool IsNullOrWhiteSpace (string value) + { + if (string.IsNullOrEmpty (value)) + return true; + + for (int i = 0; i < value.Length; i++) { + if (!char.IsWhiteSpace (value[i])) + return false; + } + + return true; + } + + internal string Encode (FormatOptions options) + { + var builder = new StringBuilder (); + + for (int i = 0; i < domains.Count; i++) { + if (IsNullOrWhiteSpace (domains[i])) + continue; + + if (builder.Length > 0) + builder.Append (','); + + builder.Append ('@'); + + if (!options.International && ParseUtils.IsInternational (domains[i])) { + var domain = ParseUtils.IdnEncode (domains[i]); + + builder.Append (domain); + } else { + builder.Append (domains[i]); + } + } + + return builder.ToString (); + } + + /// + /// Returns a string representation of the list of domains. + /// + /// + /// Each non-empty domain string will be prepended by an '@'. + /// If there are multiple domains in the list, they will be separated by a comma. + /// + /// A string representing the . + public override string ToString () + { + var builder = new StringBuilder (); + + for (int i = 0; i < domains.Count; i++) { + if (IsNullOrWhiteSpace (domains[i])) + continue; + + if (builder.Length > 0) + builder.Append (','); + + builder.Append ('@'); + + builder.Append (domains[i]); + } + + return builder.ToString (); + } + + internal event EventHandler Changed; + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + /// + /// Try to parse a list of domains. + /// + /// + /// Attempts to parse a from the text buffer starting at the + /// specified index. The index will only be updated if a was + /// successfully parsed. + /// + /// true if a was successfully parsed; + /// false otherwise. + /// The buffer to parse. + /// The index to start parsing. + /// An index of the end of the input. + /// A flag indicating whether or not an + /// exception should be thrown on error. + /// The parsed DomainList. + internal static bool TryParse (byte[] buffer, ref int index, int endIndex, bool throwOnError, out DomainList route) + { + var domains = new List (); + int startIndex = index; + string domain; + + route = null; + + do { + // skip over the '@' + index++; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete domain-list at offset: {0}", startIndex), startIndex, index); + + return false; + } + + if (!ParseUtils.TryParseDomain (buffer, ref index, endIndex, DomainSentinels, throwOnError, out domain)) + return false; + + domains.Add (domain); + + // Note: obs-domain-list allows for null domains between commas + do { + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || buffer[index] != (byte) ',') + break; + + index++; + } while (true); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, throwOnError)) + return false; + } while (index < buffer.Length && buffer[index] == (byte) '@'); + + route = new DomainList (domains); + + return true; + } + + /// + /// Try to parse a list of domains. + /// + /// + /// Attempts to parse a from the supplied text. The index + /// will only be updated if a was successfully parsed. + /// + /// true if a was successfully parsed; + /// false otherwise. + /// The text to parse. + /// The parsed DomainList. + /// + /// is null. + /// + public static bool TryParse (string text, out DomainList route) + { + int index = 0; + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + var buffer = Encoding.UTF8.GetBytes (text); + + return TryParse (buffer, ref index, buffer.Length, false, out route); + } + } +} diff --git a/src/MimeKit/EncodingConstraint.cs b/src/MimeKit/EncodingConstraint.cs new file mode 100644 index 0000000..d3ce03b --- /dev/null +++ b/src/MimeKit/EncodingConstraint.cs @@ -0,0 +1,52 @@ +// +// EncodingConstraint.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// A content encoding constraint. + /// + /// + /// Not all message transports support binary or 8-bit data, so it becomes + /// necessary to constrain the content encoding to a subset of the possible + /// Content-Transfer-Encoding values. + /// + public enum EncodingConstraint { + /// + /// There are no encoding constraints, the content may contain any byte. + /// + None, + + /// + /// The content may contain bytes with the high bit set, but must not contain any zero-bytes. + /// + EightBit, + + /// + /// The content may only contain bytes within the 7-bit ASCII range. + /// + SevenBit, + } +} diff --git a/src/MimeKit/Encodings/Base64Decoder.cs b/src/MimeKit/Encodings/Base64Decoder.cs new file mode 100644 index 0000000..ba2e5b2 --- /dev/null +++ b/src/MimeKit/Encodings/Base64Decoder.cs @@ -0,0 +1,251 @@ +// +// Base64Decoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// Incrementally decodes content encoded with the base64 encoding. + /// + /// + /// Base64 is an encoding often used in MIME to encode binary content such + /// as images and other types of multi-media to ensure that the data remains + /// intact when sent via 7bit transports such as SMTP. + /// + public class Base64Decoder : IMimeDecoder + { + static readonly byte[] base64_rank = new byte[256] { + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255, 0,255,255, + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, + 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, + }; + + uint saved; + byte bytes; + byte npad; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new base64 decoder. + /// + public Base64Decoder () + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + var decoder = new Base64Decoder (); + + decoder.saved = saved; + decoder.bytes = bytes; + decoder.npad = npad; + + return decoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Base64; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + // may require up to 3 padding bytes + return inputLength + 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + // decode every quartet into a triplet + while (inptr < inend) { + byte c = base64_rank[*inptr++]; + if (c != 0xFF) { + saved = (saved << 6) | c; + bytes++; + + if (bytes == 4) { + *outptr++ = (byte) ((saved >> 16) & 0xFF); + *outptr++ = (byte) ((saved >> 8) & 0xFF); + *outptr++ = (byte) (saved & 0xFF); + saved = 0; + bytes = 0; + + if (npad > 0) { + outptr -= npad; + npad = 0; + } + } + } + } + + // Note: we can drop 1 output character per trailing '=' (up to 2) + for (int eq = 0; inptr > input && eq < 2; ) { + inptr--; + + if (base64_rank[*inptr] != 0xFF) { + if (*inptr == '=' && outptr > output) { + if (bytes == 0) { + // we've got a full quartet, so it's safe to drop an output character. + outptr--; + } else if (npad < 2) { + // keep a record of the # of '='s at the end of the input (up to 2) + npad++; + } + + eq++; + } else { + break; + } + } + } + + return (int) (outptr - output); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + saved = 0; + bytes = 0; + npad = 0; + } + } +} diff --git a/src/MimeKit/Encodings/Base64Encoder.cs b/src/MimeKit/Encodings/Base64Encoder.cs new file mode 100644 index 0000000..5dcf0f4 --- /dev/null +++ b/src/MimeKit/Encodings/Base64Encoder.cs @@ -0,0 +1,337 @@ +// +// Base64Encoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// Incrementally encodes content using the base64 encoding. + /// + /// + /// Base64 is an encoding often used in MIME to encode binary content such + /// as images and other types of multi-media to ensure that the data remains + /// intact when sent via 7bit transports such as SMTP. + /// + public class Base64Encoder : IMimeEncoder + { + static readonly byte[] base64_alphabet = { + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, + 0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2B, 0x2F + }; + + readonly int quartetsPerLine; + readonly bool rfc2047; + int quartets; + byte saved1; + byte saved2; + byte saved; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new base64 encoder. + /// + /// true if this encoder will be used to encode rfc2047 encoded-word payloads; false otherwise. + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// + internal Base64Encoder (bool rfc2047, int maxLineLength = 72) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + quartetsPerLine = maxLineLength / 4; + this.rfc2047 = rfc2047; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new base64 encoder. + /// + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// + public Base64Encoder (int maxLineLength = 72) : this (false, maxLineLength) + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + var encoder = new Base64Encoder (rfc2047, quartetsPerLine * 4); + + encoder.quartets = quartets; + encoder.saved1 = saved1; + encoder.saved2 = saved2; + encoder.saved = saved; + + return encoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Base64; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + if (rfc2047) + return ((inputLength + 2) / 3) * 4; + + int maxLineLength = (quartetsPerLine * 4) + 1; + int maxInputPerLine = quartetsPerLine * 3; + + return (((inputLength + 2) / maxInputPerLine) * maxLineLength) + maxLineLength; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + unsafe int Encode (byte* input, int length, byte* output) + { + if (length == 0) + return 0; + + int remaining = length; + byte* outptr = output; + byte* inptr = input; + + if (length + saved > 2) { + byte* inend = inptr + length - 2; + int c1, c2, c3; + + c1 = saved < 1 ? *inptr++ : saved1; + c2 = saved < 2 ? *inptr++ : saved2; + c3 = *inptr++; + + do { + // encode our triplet into a quartet + *outptr++ = base64_alphabet[c1 >> 2]; + *outptr++ = base64_alphabet[(c2 >> 4) | ((c1 & 0x3) << 4)]; + *outptr++ = base64_alphabet[((c2 & 0x0f) << 2) | (c3 >> 6)]; + *outptr++ = base64_alphabet[c3 & 0x3f]; + + // encode 18 quartets per line + if (!rfc2047 && (++quartets) >= quartetsPerLine) { + *outptr++ = (byte) '\n'; + quartets = 0; + } + + if (inptr >= inend) + break; + + c1 = *inptr++; + c2 = *inptr++; + c3 = *inptr++; + } while (true); + + remaining = 2 - (int) (inptr - inend); + saved = 0; + } + + if (remaining > 0) { + // At this point, saved can only be 0 or 1. + if (saved == 0) { + // We can have up to 2 remaining input bytes. + saved = (byte) remaining; + saved1 = *inptr++; + if (remaining == 2) + saved2 = *inptr; + else + saved2 = 0; + } else { + // We have 1 remaining input byte. + saved2 = *inptr++; + saved = 2; + } + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Encode (inptr + startIndex, length, outptr); + } + } + } + + unsafe int Flush (byte* input, int length, byte* output) + { + byte* outptr = output; + + if (length > 0) + outptr += Encode (input, length, output); + + if (saved >= 1) { + int c1 = saved1; + int c2 = saved2; + + *outptr++ = base64_alphabet[c1 >> 2]; + *outptr++ = base64_alphabet[c2 >> 4 | ((c1 & 0x3) << 4)]; + if (saved == 2) + *outptr++ = base64_alphabet[(c2 & 0x0f) << 2]; + else + *outptr++ = (byte) '='; + *outptr++ = (byte) '='; + } + + if (!rfc2047) + *outptr++ = (byte) '\n'; + + Reset (); + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Flush (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + quartets = 0; + saved1 = 0; + saved2 = 0; + saved = 0; + } + } +} diff --git a/src/MimeKit/Encodings/HexDecoder.cs b/src/MimeKit/Encodings/HexDecoder.cs new file mode 100644 index 0000000..d7fb2e5 --- /dev/null +++ b/src/MimeKit/Encodings/HexDecoder.cs @@ -0,0 +1,232 @@ +// +// HexDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally decodes content encoded with a Uri hex encoding. + /// + /// + /// This is mostly meant for decoding parameter values encoded using + /// the rules specified by rfc2184 and rfc2231. + /// + public class HexDecoder : IMimeDecoder + { + enum HexDecoderState : byte { + PassThrough, + Percent, + DecodeByte + } + + HexDecoderState state; + byte saved; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new hex decoder. + /// + public HexDecoder () + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + var decoder = new HexDecoder (); + + decoder.state = state; + decoder.saved = saved; + + return decoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Default; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + // add an extra 3 bytes for the saved input byte from previous decode step (in case it is invalid hex) + return inputLength + 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + byte c; + + while (inptr < inend) { + switch (state) { + case HexDecoderState.PassThrough: + while (inptr < inend) { + c = *inptr++; + + if (c == '%') { + state = HexDecoderState.Percent; + break; + } + + *outptr++ = c; + } + break; + case HexDecoderState.Percent: + c = *inptr++; + state = HexDecoderState.DecodeByte; + saved = c; + break; + case HexDecoderState.DecodeByte: + c = *inptr++; + if (c.IsXDigit () && saved.IsXDigit ()) { + saved = saved.ToXDigit (); + c = c.ToXDigit (); + + *outptr++ = (byte) ((saved << 4) | c); + } else { + // invalid encoded sequence - pass it through undecoded + *outptr++ = (byte) '%'; + *outptr++ = saved; + *outptr++ = c; + } + + state = HexDecoderState.PassThrough; + break; + } + } + + return (int) (outptr - output); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + state = HexDecoderState.PassThrough; + saved = 0; + } + } +} diff --git a/src/MimeKit/Encodings/HexEncoder.cs b/src/MimeKit/Encodings/HexEncoder.cs new file mode 100644 index 0000000..5a7beae --- /dev/null +++ b/src/MimeKit/Encodings/HexEncoder.cs @@ -0,0 +1,216 @@ +// +// HexEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally encodes content using a Uri hex encoding. + /// + /// + /// This is mostly meant for decoding parameter values encoded using + /// the rules specified by rfc2184 and rfc2231. + /// + public class HexEncoder : IMimeEncoder + { + static readonly byte[] hex_alphabet = new byte[16] { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7' + 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F' + }; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new hex encoder. + /// + public HexEncoder () + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + return new HexEncoder (); + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Default; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return inputLength * 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + static unsafe int Encode (byte* input, int length, byte* output) + { + if (length == 0) + return 0; + + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + while (inptr < inend) { + byte c = *inptr++; + + if (c.IsAttr ()) { + *outptr++ = c; + } else { + *outptr++ = (byte) '%'; + *outptr++ = hex_alphabet[(c >> 4) & 0x0f]; + *outptr++ = hex_alphabet[c & 0x0f]; + } + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Encode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + return Encode (input, startIndex, length, output); + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + } + } +} diff --git a/src/MimeKit/Encodings/IMimeDecoder.cs b/src/MimeKit/Encodings/IMimeDecoder.cs new file mode 100644 index 0000000..dec3d76 --- /dev/null +++ b/src/MimeKit/Encodings/IMimeDecoder.cs @@ -0,0 +1,117 @@ +// +// IMimeDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Encodings { + /// + /// An interface for incrementally decoding content. + /// + /// + /// An interface for incrementally decoding content. + /// + public interface IMimeDecoder + { + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + ContentEncoding Encoding { get; } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + IMimeDecoder Clone (); + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + int EstimateOutputLength (int inputLength); + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + unsafe int Decode (byte* input, int length, byte* output); + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + int Decode (byte[] input, int startIndex, int length, byte[] output); + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + void Reset (); + } +} diff --git a/src/MimeKit/Encodings/IMimeEncoder.cs b/src/MimeKit/Encodings/IMimeEncoder.cs new file mode 100644 index 0000000..741bb12 --- /dev/null +++ b/src/MimeKit/Encodings/IMimeEncoder.cs @@ -0,0 +1,132 @@ +// +// IMimeEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Encodings { + /// + /// An interface for incrementally encoding content. + /// + /// + /// An interface for incrementally encoding content. + /// + public interface IMimeEncoder + { + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + ContentEncoding Encoding { get; } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + IMimeEncoder Clone (); + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + int EstimateOutputLength (int inputLength); + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + int Encode (byte[] input, int startIndex, int length, byte[] output); + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + int Flush (byte[] input, int startIndex, int length, byte[] output); + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + void Reset (); + } +} diff --git a/src/MimeKit/Encodings/PassThroughDecoder.cs b/src/MimeKit/Encodings/PassThroughDecoder.cs new file mode 100644 index 0000000..87aaf73 --- /dev/null +++ b/src/MimeKit/Encodings/PassThroughDecoder.cs @@ -0,0 +1,172 @@ +// +// PassThroughDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// A pass-through decoder implementing the interface. + /// + /// + /// Simply copies data as-is from the input buffer into the output buffer. + /// + public class PassThroughDecoder : IMimeDecoder + { + /// + /// Initialize a new instance of the class. + /// + /// The encoding to return in the property. + /// + /// Creates a new pass-through decoder. + /// + public PassThroughDecoder (ContentEncoding encoding) + { + Encoding = encoding; + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + return new PassThroughDecoder (Encoding); + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get; private set; + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return inputLength; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Copies the input buffer into the output buffer, verbatim. + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + while (inptr < inend) + *outptr++ = *inptr++; + + return length; + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Copies the input buffer into the output buffer, verbatim. + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + } + } +} diff --git a/src/MimeKit/Encodings/PassThroughEncoder.cs b/src/MimeKit/Encodings/PassThroughEncoder.cs new file mode 100644 index 0000000..3dc50db --- /dev/null +++ b/src/MimeKit/Encodings/PassThroughEncoder.cs @@ -0,0 +1,178 @@ +// +// PassThroughEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// A pass-through encoder implementing the interface. + /// + /// + /// Simply copies data as-is from the input buffer into the output buffer. + /// + public class PassThroughEncoder : IMimeEncoder + { + /// + /// Initialize a new instance of the class. + /// + /// The encoding to return in the property. + /// + /// Creates a new pass-through encoder. + /// + public PassThroughEncoder (ContentEncoding encoding) + { + Encoding = encoding; + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + return new PassThroughEncoder (Encoding); + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get; private set; + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return inputLength; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Copies the input buffer into the output buffer, verbatim. + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + Buffer.BlockCopy (input, startIndex, output, 0, length); + + return length; + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Copies the input buffer into the output buffer, verbatim. + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + return Encode (input, startIndex, length, output); + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + } + } +} diff --git a/src/MimeKit/Encodings/QEncoder.cs b/src/MimeKit/Encodings/QEncoder.cs new file mode 100644 index 0000000..e8a5ba3 --- /dev/null +++ b/src/MimeKit/Encodings/QEncoder.cs @@ -0,0 +1,260 @@ +// +// QEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Q-Encoding mode. + /// + /// + /// The encoding mode for the 'Q' encoding used in rfc2047. + /// + public enum QEncodeMode : byte { + /// + /// A mode for encoding phrases, as defined by rfc822. + /// + Phrase, + + /// + /// A mode for encoding text. + /// + Text + } + + /// + /// Incrementally encodes content using a variation of the quoted-printable encoding + /// that is specifically meant to be used for rfc2047 encoded-word tokens. + /// + /// + /// The Q-Encoding is an encoding often used in MIME to encode textual content outside + /// of the ASCII range within an rfc2047 encoded-word token in order to ensure that + /// the text remains intact when sent via 7bit transports such as SMTP. + /// + public class QEncoder : IMimeEncoder + { + static readonly byte[] hex_alphabet = new byte[16] { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7' + 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F' + }; + + readonly CharType mask; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new rfc2047 quoted-printable encoder. + /// + /// The rfc2047 encoding mode. + public QEncoder (QEncodeMode mode) + { + mask = mode == QEncodeMode.Phrase ? CharType.IsEncodedPhraseSafe : CharType.IsEncodedWordSafe; + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + return new QEncoder (mask == CharType.IsEncodedPhraseSafe ? QEncodeMode.Phrase : QEncodeMode.Text); + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.QuotedPrintable; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return inputLength * 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + unsafe int Encode (byte* input, int length, byte* output) + { + if (length == 0) + return 0; + + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + while (inptr < inend) { + byte c = *inptr++; + + if (c == ' ') { + *outptr++ = (byte) '_'; + } else if (c != '_' && c.IsType (mask)) { + *outptr++ = c; + } else { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(c >> 4) & 0x0f]; + *outptr++ = hex_alphabet[c & 0x0f]; + } + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Encode (inptr + startIndex, length, outptr); + } + } + } + + unsafe int Flush (byte* input, int length, byte* output) + { + byte* outptr = output; + + if (length > 0) + outptr += Encode (input, length, output); + + Reset (); + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Flush (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + } + } +} diff --git a/src/MimeKit/Encodings/QuotedPrintableDecoder.cs b/src/MimeKit/Encodings/QuotedPrintableDecoder.cs new file mode 100644 index 0000000..eee3f16 --- /dev/null +++ b/src/MimeKit/Encodings/QuotedPrintableDecoder.cs @@ -0,0 +1,277 @@ +// +// QuotedPrintableDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally decodes content encoded with the quoted-printable encoding. + /// + /// + /// Quoted-Printable is an encoding often used in MIME to textual content outside + /// of the ASCII range in order to ensure that the text remains intact when sent + /// via 7bit transports such as SMTP. + /// + public class QuotedPrintableDecoder : IMimeDecoder + { + enum QpDecoderState : byte { + PassThrough, + EqualSign, + SoftBreak, + DecodeByte + } + + readonly bool rfc2047; + QpDecoderState state; + byte saved; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new quoted-printable decoder. + /// + /// + /// true if this decoder will be used to decode rfc2047 encoded-word payloads; false otherwise. + /// + public QuotedPrintableDecoder (bool rfc2047) + { + this.rfc2047 = rfc2047; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new quoted-printable decoder. + /// + public QuotedPrintableDecoder () : this (false) + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + var decoder = new QuotedPrintableDecoder (rfc2047); + + decoder.state = state; + decoder.saved = saved; + + return decoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.QuotedPrintable; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + // add an extra 3 bytes for the saved input byte from previous decode step (in case it is invalid hex) + return inputLength + 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + byte c; + + while (inptr < inend) { + switch (state) { + case QpDecoderState.PassThrough: + while (inptr < inend) { + c = *inptr++; + + if (c == '=') { + state = QpDecoderState.EqualSign; + break; + } else if (rfc2047 && c == '_') { + *outptr++ = (byte) ' '; + } else { + *outptr++ = c; + } + } + break; + case QpDecoderState.EqualSign: + c = *inptr++; + + if (c.IsXDigit ()) { + state = QpDecoderState.DecodeByte; + saved = c; + } else if (c == '=') { + // invalid encoded sequence - pass it through undecoded + *outptr++ = (byte) '='; + } else if (c == '\r') { + state = QpDecoderState.SoftBreak; + } else if (c == '\n') { + state = QpDecoderState.PassThrough; + } else { + // invalid encoded sequence - pass it through undecoded + state = QpDecoderState.PassThrough; + *outptr++ = (byte) '='; + *outptr++ = c; + } + break; + case QpDecoderState.SoftBreak: + state = QpDecoderState.PassThrough; + c = *inptr++; + + if (c != '\n') { + // invalid encoded sequence - pass it through undecoded + *outptr++ = (byte) '='; + *outptr++ = (byte) '\r'; + *outptr++ = c; + } + break; + case QpDecoderState.DecodeByte: + c = *inptr++; + if (c.IsXDigit ()) { + saved = saved.ToXDigit (); + c = c.ToXDigit (); + + *outptr++ = (byte) ((saved << 4) | c); + } else { + // invalid encoded sequence - pass it through undecoded + *outptr++ = (byte) '='; + *outptr++ = saved; + *outptr++ = c; + } + + state = QpDecoderState.PassThrough; + break; + } + } + + return (int) (outptr - output); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + state = QpDecoderState.PassThrough; + saved = 0; + } + } +} diff --git a/src/MimeKit/Encodings/QuotedPrintableEncoder.cs b/src/MimeKit/Encodings/QuotedPrintableEncoder.cs new file mode 100644 index 0000000..ff7d4fc --- /dev/null +++ b/src/MimeKit/Encodings/QuotedPrintableEncoder.cs @@ -0,0 +1,317 @@ +// +// QuotedPrintableEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally encodes content using the quoted-printable encoding. + /// + /// + /// Quoted-Printable is an encoding often used in MIME to encode textual content + /// outside of the ASCII range in order to ensure that the text remains intact + /// when sent via 7bit transports such as SMTP. + /// + public class QuotedPrintableEncoder : IMimeEncoder + { + static readonly byte[] hex_alphabet = new byte[16] { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // '0' -> '7' + 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, // '8' -> 'F' + }; + + const int TripletsPerLine = 23; + const int DesiredLineLength = TripletsPerLine * 3; + const int MaxLineLength = DesiredLineLength + 2; // "=\n" + + short currentLineLength; + short saved; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new quoted-printable encoder. + /// + public QuotedPrintableEncoder () + { + Reset (); + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + var encoder = new QuotedPrintableEncoder (); + + encoder.currentLineLength = currentLineLength; + encoder.saved = saved; + + return encoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.QuotedPrintable; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return ((inputLength / TripletsPerLine) * MaxLineLength) + MaxLineLength; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + unsafe int Encode (byte* input, int length, byte* output) + { + if (length == 0) + return 0; + + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + while (inptr < inend) { + byte c = *inptr++; + + if (c == (byte) '\r') { + if (saved != -1) { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(saved >> 4) & 0x0f]; + *outptr++ = hex_alphabet[saved & 0x0f]; + currentLineLength += 3; + } + + saved = c; + } else if (c == (byte) '\n') { + if (saved != -1 && saved != '\r') { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(saved >> 4) & 0x0f]; + *outptr++ = hex_alphabet[saved & 0x0f]; + } + + *outptr++ = (byte) '\n'; + currentLineLength = 0; + saved = -1; + } else { + if (saved != -1) { + byte b = (byte) saved; + + if (b.IsQpSafe ()) { + *outptr++ = b; + currentLineLength++; + } else { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(saved >> 4) & 0x0f]; + *outptr++ = hex_alphabet[saved & 0x0f]; + } + } + + if (currentLineLength > DesiredLineLength) { + *outptr++ = (byte) '='; + *outptr++ = (byte) '\n'; + currentLineLength = 0; + } + + if (c.IsQpSafe ()) { + // delay output of whitespace character + if (c.IsBlank ()) { + saved = c; + } else { + *outptr++ = c; + currentLineLength++; + saved = -1; + } + } else { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(c >> 4) & 0x0f]; + *outptr++ = hex_alphabet[c & 0x0f]; + currentLineLength += 3; + saved = -1; + } + } + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Encode (inptr + startIndex, length, outptr); + } + } + } + + unsafe int Flush (byte* input, int length, byte* output) + { + byte* outptr = output; + + if (length > 0) + outptr += Encode (input, length, output); + + if (saved != -1) { + // spaces and tabs must be encoded if they the last character on the line + byte c = (byte) saved; + + if (c.IsBlank () || !c.IsQpSafe ()) { + *outptr++ = (byte) '='; + *outptr++ = hex_alphabet[(saved >> 4) & 0xf]; + *outptr++ = hex_alphabet[saved & 0xf]; + } else { + *outptr++ = c; + } + + // we end with =\n so that the \n isn't interpreted as + // a real \n when it gets decoded later + *outptr++ = (byte) '='; + *outptr++ = (byte) '\n'; + } + + Reset (); + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Flush (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + currentLineLength = 0; + saved = -1; + } + } +} diff --git a/src/MimeKit/Encodings/UUDecoder.cs b/src/MimeKit/Encodings/UUDecoder.cs new file mode 100644 index 0000000..59a86c8 --- /dev/null +++ b/src/MimeKit/Encodings/UUDecoder.cs @@ -0,0 +1,418 @@ +// +// UUDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// Incrementally decodes content encoded with the Unix-to-Unix encoding. + /// + /// + /// The UUEncoding is an encoding that predates MIME and was used to encode + /// binary content such as images and other types of multi-media to ensure + /// that the data remained intact when sent via 7bit transports such as SMTP. + /// These days, the UUEncoding has largely been deprecated in favour of + /// the base64 encoding, however, some older mail clients still use it. + /// + public class UUDecoder : IMimeDecoder + { + static readonly byte[] uudecode_rank = new byte[256] { + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + }; + + enum UUDecoderState : byte { + ExpectBegin, + B, + Be, + Beg, + Begi, + Begin, + ExpectPayload, + Payload, + Ended, + } + + readonly UUDecoderState initial; + UUDecoderState state; + byte nsaved; + byte uulen; + uint saved; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new Unix-to-Unix decoder. + /// + /// + /// If true, decoding begins immediately rather than after finding a begin-line. + /// + public UUDecoder (bool payloadOnly) + { + initial = payloadOnly ? UUDecoderState.Payload : UUDecoderState.ExpectBegin; + Reset (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new Unix-to-Unix decoder. + /// + public UUDecoder () : this (false) + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + var decoder = new UUDecoder (initial == UUDecoderState.Payload); + + decoder.state = state; + decoder.nsaved = nsaved; + decoder.saved = saved; + decoder.uulen = uulen; + + return decoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.UUEncode; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + // add an extra 3 bytes for the saved input bytes from previous decode step + return inputLength + 3; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + unsafe byte* ScanBeginMarker (byte* inptr, byte* inend) + { + while (inptr < inend) { + if (state == UUDecoderState.ExpectBegin) { + if (nsaved != 0 && nsaved != (byte) '\n') { + while (inptr < inend && *inptr != (byte) '\n') + inptr++; + + if (inptr == inend) { + nsaved = *(inptr - 1); + return inptr; + } + + nsaved = *inptr++; + if (inptr == inend) + return inptr; + } + + nsaved = *inptr++; + if (nsaved != (byte) 'b') + continue; + + state = UUDecoderState.B; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.B) { + nsaved = *inptr++; + if (nsaved != (byte) 'e') { + state = UUDecoderState.ExpectBegin; + continue; + } + + state = UUDecoderState.Be; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.Be) { + nsaved = *inptr++; + if (nsaved != (byte) 'g') { + state = UUDecoderState.ExpectBegin; + continue; + } + + state = UUDecoderState.Beg; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.Beg) { + nsaved = *inptr++; + if (nsaved != (byte) 'i') { + state = UUDecoderState.ExpectBegin; + continue; + } + + state = UUDecoderState.Begi; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.Begi) { + nsaved = *inptr++; + if (nsaved != (byte) 'n') { + state = UUDecoderState.ExpectBegin; + continue; + } + + state = UUDecoderState.Begin; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.Begin) { + nsaved = *inptr++; + if (nsaved != (byte) ' ') { + state = UUDecoderState.ExpectBegin; + continue; + } + + state = UUDecoderState.ExpectPayload; + if (inptr == inend) + return inptr; + } + + if (state == UUDecoderState.ExpectPayload) { + while (inptr < inend && *inptr != (byte) '\n') + inptr++; + + if (inptr == inend) + return inptr; + + state = UUDecoderState.Payload; + nsaved = 0; + + return inptr + 1; + } + } + + return inptr; + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + if (state == UUDecoderState.Ended) + return 0; + + bool last_was_eoln = uulen == 0; + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + byte c; + + if (state < UUDecoderState.Payload) { + if ((inptr = ScanBeginMarker (inptr, inend)) == inend) + return 0; + } + + while (inptr < inend) { + if (*inptr == (byte) '\r') { + inptr++; + continue; + } + + if (*inptr == (byte) '\n') { + last_was_eoln = true; + inptr++; + continue; + } + + if (uulen == 0 || last_was_eoln) { + // first octet on a line is the uulen octet + uulen = uudecode_rank[*inptr]; + last_was_eoln = false; + if (uulen == 0) { + state = UUDecoderState.Ended; + break; + } + + inptr++; + continue; + } + + c = *inptr++; + + if (uulen > 0) { + // save the byte + saved = (saved << 8) | c; + nsaved++; + + if (nsaved == 4) { + byte b0 = (byte) ((saved >> 24) & 0xFF); + byte b1 = (byte) ((saved >> 16) & 0xFF); + byte b2 = (byte) ((saved >> 8) & 0xFF); + byte b3 = (byte) (saved & 0xFF); + + if (uulen >= 3) { + *outptr++ = (byte) (uudecode_rank[b0] << 2 | uudecode_rank[b1] >> 4); + *outptr++ = (byte) (uudecode_rank[b1] << 4 | uudecode_rank[b2] >> 2); + *outptr++ = (byte) (uudecode_rank[b2] << 6 | uudecode_rank[b3]); + uulen -= 3; + } else { + if (uulen >= 1) { + *outptr++ = (byte) (uudecode_rank[b0] << 2 | uudecode_rank[b1] >> 4); + uulen--; + } + + if (uulen >= 1) { + *outptr++ = (byte) (uudecode_rank[b1] << 4 | uudecode_rank[b2] >> 2); + uulen--; + } + } + + nsaved = 0; + saved = 0; + } + } else { + break; + } + } + + return (int) (outptr - output); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + state = initial; + nsaved = 0; + saved = 0; + uulen = 0; + } + } +} diff --git a/src/MimeKit/Encodings/UUEncoder.cs b/src/MimeKit/Encodings/UUEncoder.cs new file mode 100644 index 0000000..eef18cd --- /dev/null +++ b/src/MimeKit/Encodings/UUEncoder.cs @@ -0,0 +1,375 @@ +// +// UUEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Encodings { + /// + /// Incrementally encodes content using the Unix-to-Unix encoding. + /// + /// + /// The UUEncoding is an encoding that predates MIME and was used to encode + /// binary content such as images and other types of multi-media to ensure + /// that the data remained intact when sent via 7bit transports such as SMTP. + /// These days, the UUEncoding has largely been deprecated in favour of + /// the base64 encoding, however, some older mail clients still use it. + /// + public class UUEncoder : IMimeEncoder + { + const int MaxInputPerLine = 45; + const int MaxOutputPerLine = ((MaxInputPerLine / 3) * 4) + 2; + + readonly byte[] uubuf = new byte[60]; + uint saved; + byte nsaved; + byte uulen; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new Unix-to-Unix encoder. + /// + public UUEncoder () + { + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + var encoder = new UUEncoder (); + + Buffer.BlockCopy (uubuf, 0, encoder.uubuf, 0, uubuf.Length); + encoder.nsaved = nsaved; + encoder.saved = saved; + encoder.uulen = uulen; + + return encoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.UUEncode; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return (((inputLength + 2) / MaxInputPerLine) * MaxOutputPerLine) + MaxOutputPerLine + 2; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + static byte Encode (int c) + { + return c != 0 ? (byte) (c + 0x20) : (byte) '`'; + } + + unsafe int Encode (byte* input, int length, byte[] outbuf, byte* output, byte *uuptr) + { + if (length == 0) + return 0; + + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + byte* bufptr; + byte b0, b1, b2; + + if ((length + nsaved + uulen) < 45) { + // not enough input to write a full uuencoded line + bufptr = uuptr + ((uulen / 3) * 4); + } else { + bufptr = outptr + 1; + + if (uulen > 0) { + // copy the previous call's uubuf to output + int n = (uulen / 3) * 4; + + Buffer.BlockCopy (uubuf, 0, outbuf, 1, n); + bufptr += n; + } + } + + if (nsaved == 2) { + b0 = (byte) ((saved >> 8) & 0xFF); + b1 = (byte) (saved & 0xFF); + b2 = *inptr++; + nsaved = 0; + saved = 0; + + // convert 3 input bytes into 4 uuencoded bytes + *bufptr++ = Encode ((b0 >> 2) & 0x3F); + *bufptr++ = Encode (((b0 << 4) | ((b1 >> 4) & 0x0F)) & 0x3F); + *bufptr++ = Encode (((b1 << 2) | ((b2 >> 6) & 0x03)) & 0x3F); + *bufptr++ = Encode (b2 & 0x3F); + + uulen += 3; + } else if (nsaved == 1) { + if ((inptr + 2) < inend) { + b0 = (byte) (saved & 0xFF); + b1 = *inptr++; + b2 = *inptr++; + nsaved = 0; + saved = 0; + + // convert 3 input bytes into 4 uuencoded bytes + *bufptr++ = Encode ((b0 >> 2) & 0x3F); + *bufptr++ = Encode (((b0 << 4) | ((b1 >> 4) & 0x0F)) & 0x3F); + *bufptr++ = Encode (((b1 << 2) | ((b2 >> 6) & 0x03)) & 0x3F); + *bufptr++ = Encode (b2 & 0x3F); + + uulen += 3; + } else { + while (inptr < inend) { + saved = (saved << 8) | *inptr++; + nsaved++; + } + } + } + + do { + while (uulen < 45 && (inptr + 2) < inend) { + b0 = *inptr++; + b1 = *inptr++; + b2 = *inptr++; + + // convert 3 input bytes into 4 uuencoded bytes + *bufptr++ = Encode ((b0 >> 2) & 0x3F); + *bufptr++ = Encode (((b0 << 4) | ((b1 >> 4) & 0x0F)) & 0x3F); + *bufptr++ = Encode (((b1 << 2) | ((b2 >> 6) & 0x03)) & 0x3F); + *bufptr++ = Encode (b2 & 0x3F); + + uulen += 3; + } + + if (uulen >= 45) { + // output the uu line length + *outptr = Encode (uulen); + outptr += ((uulen / 3) * 4) + 1; + *outptr++ = (byte) '\n'; + uulen = 0; + + if ((inptr + 45) <= inend) { + // we have enough input to output another full line + bufptr = outptr + 1; + } else { + bufptr = uuptr; + } + } else { + // not enough input to continue... + while (inptr < inend) { + saved = (saved << 8) | *inptr++; + nsaved++; + } + } + } while (inptr < inend); + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output, uuptr = uubuf) { + return Encode (inptr + startIndex, length, output, outptr, uuptr); + } + } + } + + unsafe int Flush (byte* input, int length, byte[] outbuf, byte* output, byte* uuptr) + { + byte* outptr = output; + + if (length > 0) + outptr += Encode (input, length, outbuf, output, uuptr); + + byte* bufptr = uuptr + ((uulen / 3) * 4); + byte uufill = 0; + + if (nsaved > 0) { + while (nsaved < 3) { + saved <<= 8; + uufill++; + nsaved++; + } + + if (nsaved == 3) { + // convert 3 input bytes into 4 uuencoded bytes + byte b0, b1, b2; + + b0 = (byte) ((saved >> 16) & 0xFF); + b1 = (byte) ((saved >> 8) & 0xFF); + b2 = (byte) (saved & 0xFF); + + *bufptr++ = Encode ((b0 >> 2) & 0x3F); + *bufptr++ = Encode (((b0 << 4) | ((b1 >> 4) & 0x0F)) & 0x3F); + *bufptr++ = Encode (((b1 << 2) | ((b2 >> 6) & 0x03)) & 0x3F); + *bufptr++ = Encode (b2 & 0x3F); + + uulen += 3; + nsaved = 0; + saved = 0; + } + } + + if (uulen > 0) { + int n = (uulen / 3) * 4; + + *outptr++ = Encode ((uulen - uufill) & 0xFF); + Buffer.BlockCopy (uubuf, 0, outbuf, (int) (outptr - output), n); + outptr += n; + + *outptr++ = (byte) '\n'; + uulen = 0; + } + + *outptr++ = Encode (uulen & 0xFF); + *outptr++ = (byte) '\n'; + + Reset (); + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output, uuptr = uubuf) { + return Flush (inptr + startIndex, length, output, outptr, uuptr); + } + } + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + nsaved = 0; + saved = 0; + uulen = 0; + } + } +} diff --git a/src/MimeKit/Encodings/YDecoder.cs b/src/MimeKit/Encodings/YDecoder.cs new file mode 100644 index 0000000..61d8cbb --- /dev/null +++ b/src/MimeKit/Encodings/YDecoder.cs @@ -0,0 +1,544 @@ +// +// YDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally decodes content encoded with the yEnc encoding. + /// + /// + /// The yEncoding is an encoding that is most commonly used with Usenet and + /// is a binary encoding that includes a 32-bit cyclic redundancy check. + /// For more information, see www.yenc.org. + /// + public class YDecoder : IMimeDecoder + { + enum YDecoderState : byte { + ExpectYBegin, + YBeginEqual, + YBeginEqualY, + YBeginEqualYB, + YBeginEqualYBe, + YBeginEqualYBeg, + YBeginEqualYBegi, + YBeginEqualYBegin, + ExpectYBeginNewLine, + + ExpectYPartOrPayload, + + YPartEqual, + YPartEqualY, + YPartEqualYP, + YPartEqualYPa, + YPartEqualYPar, + YPartEqualYPart, + ExpectYPartNewLine, + + Payload, + Ended, + } + + readonly YDecoderState initial; + YDecoderState state; + bool escaped; + byte octet; + bool eoln; + Crc32 crc; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new yEnc decoder. + /// + /// + /// If true, decoding begins immediately rather than after finding an =ybegin line. + /// + public YDecoder (bool payloadOnly) + { + initial = payloadOnly ? YDecoderState.Payload : YDecoderState.ExpectYBegin; + crc = new Crc32 (-1); + Reset (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new yEnc decoder. + /// + public YDecoder () : this (false) + { + } + + /// + /// Gets the checksum. + /// + /// + /// Gets the checksum. + /// + /// The checksum. + public int Checksum { + get { return crc.Checksum; } + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current decoder. + /// + /// A new with identical state. + public IMimeDecoder Clone () + { + var decoder = new YDecoder (initial == YDecoderState.Payload); + + decoder.crc = crc.Clone (); + decoder.escaped = escaped; + decoder.state = state; + decoder.octet = octet; + decoder.eoln = eoln; + + return decoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Default; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to decode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return inputLength; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the decoded input.", nameof (output)); + } + + unsafe byte* ScanYBeginMarker (byte* inptr, byte* inend) + { + while (inptr < inend) { + if (state == YDecoderState.ExpectYBegin) { + if (octet != (byte) '\n') { + while (inptr < inend && *inptr != (byte) '\n') + inptr++; + + if (inptr == inend) { + octet = *(inptr - 1); + break; + } + + octet = *inptr++; + if (inptr == inend) + break; + } + + octet = *inptr++; + if (octet != (byte) '=') + continue; + + state = YDecoderState.YBeginEqual; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqual) { + octet = *inptr++; + if (octet != (byte) 'y') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualY; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualY) { + octet = *inptr++; + if (octet != (byte) 'b') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualYB; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualYB) { + octet = *inptr++; + if (octet != (byte) 'e') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualYBe; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualYBe) { + octet = *inptr++; + if (octet != (byte) 'g') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualYBeg; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualYBeg) { + octet = *inptr++; + if (octet != (byte) 'i') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualYBegi; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualYBegi) { + octet = *inptr++; + if (octet != (byte) 'n') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.YBeginEqualYBegin; + if (inptr == inend) + break; + } + + if (state == YDecoderState.YBeginEqualYBegin) { + octet = *inptr++; + if (octet != (byte) ' ') { + state = YDecoderState.ExpectYBegin; + continue; + } + + state = YDecoderState.ExpectYBeginNewLine; + if (inptr == inend) + break; + } + + if (state == YDecoderState.ExpectYBeginNewLine) { + while (inptr < inend && *inptr != (byte) '\n') + inptr++; + + if (inptr == inend) { + octet = *(inptr - 1); + break; + } + + state = YDecoderState.ExpectYPartOrPayload; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.ExpectYPartOrPayload) { + if (*inptr != (byte) '=') { + state = YDecoderState.Payload; + escaped = false; + eoln = true; + break; + } + + state = YDecoderState.YPartEqual; + octet = *inptr++; + escaped = true; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqual) { + if (*inptr != (byte) 'y') { + state = YDecoderState.Payload; + escaped = false; + eoln = true; + return inptr; + } + + state = YDecoderState.YPartEqualY; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqualY) { + if (*inptr == (byte) 'e') { + // we got an "=ye" which can only be an "=yend" + state = YDecoderState.Ended; + return inptr; + } + + if (*inptr != (byte) 'p') { + state = YDecoderState.ExpectYBeginNewLine; + continue; + } + + state = YDecoderState.YPartEqualYP; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqualYP) { + if (*inptr != (byte) 'a') { + state = YDecoderState.ExpectYBeginNewLine; + continue; + } + + state = YDecoderState.YPartEqualYPa; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqualYPa) { + if (*inptr != (byte) 'r') { + state = YDecoderState.ExpectYBeginNewLine; + continue; + } + + state = YDecoderState.YPartEqualYPar; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqualYPar) { + if (*inptr != (byte) 't') { + state = YDecoderState.ExpectYBeginNewLine; + continue; + } + + state = YDecoderState.YPartEqualYPart; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.YPartEqualYPart) { + if (*inptr != (byte) ' ') { + state = YDecoderState.ExpectYBeginNewLine; + continue; + } + + state = YDecoderState.ExpectYPartNewLine; + octet = *inptr++; + + if (inptr == inend) + break; + } + + if (state == YDecoderState.ExpectYPartNewLine) { + while (inptr < inend && *inptr != (byte) '\n') + inptr++; + + if (inptr == inend) { + octet = *(inptr - 1); + break; + } + + state = YDecoderState.Payload; + octet = *inptr++; + escaped = false; + eoln = true; + break; + } + } + + return inptr; + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// A pointer to the beginning of the input buffer. + /// The length of the input buffer. + /// A pointer to the beginning of the output buffer. + public unsafe int Decode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + if (state < YDecoderState.Payload) { + if ((inptr = ScanYBeginMarker (inptr, inend)) == inend) + return 0; + } + + if (state == YDecoderState.Ended) + return 0; + + while (inptr < inend) { + octet = *inptr++; + + if (octet == (byte) '\r') { + escaped = false; + continue; + } + + if (octet == (byte) '\n') { + escaped = false; + eoln = true; + continue; + } + + if (escaped) { + if (eoln && octet == (byte) 'y') { + // this can only be =yend + state = YDecoderState.Ended; + break; + } + + escaped = false; + eoln = false; + octet -= 64; + } else if (octet == (byte) '=') { + escaped = true; + continue; + } else { + eoln = false; + } + + octet -= 42; + + crc.Update (octet); + *outptr++ = octet; + } + + return (int) (outptr - output); + } + + /// + /// Decodes the specified input into the output buffer. + /// + /// + /// Decodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// decoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Decode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Decode (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the decoder. + /// + /// + /// Resets the state of the decoder. + /// + public void Reset () + { + octet = (byte) '\n'; + state = initial; + escaped = false; + eoln = true; + + crc.Reset (); + } + } +} diff --git a/src/MimeKit/Encodings/YEncoder.cs b/src/MimeKit/Encodings/YEncoder.cs new file mode 100644 index 0000000..0873eec --- /dev/null +++ b/src/MimeKit/Encodings/YEncoder.cs @@ -0,0 +1,272 @@ +// +// YEncoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; + +namespace MimeKit.Encodings { + /// + /// Incrementally encodes content using the yEnc encoding. + /// + /// + /// The yEncoding is an encoding that is most commonly used with Usenet and + /// is a binary encoding that includes a 32-bit cyclic redundancy check. + /// For more information, see www.yenc.org. + /// + public class YEncoder : IMimeEncoder + { + readonly int lineLength; + byte octets; + Crc32 crc; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new yEnc encoder. + /// + /// The line length to use. + /// + /// is not within the range of 60 to 998. + /// + public YEncoder (int maxLineLength = 128) + { + if (maxLineLength < 60 || maxLineLength > 998) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + lineLength = maxLineLength; + crc = new Crc32 (-1); + Reset (); + } + + /// + /// Gets the checksum. + /// + /// + /// Gets the checksum. + /// + /// The checksum. + public int Checksum { + get { return crc.Checksum; } + } + + /// + /// Clone the with its current state. + /// + /// + /// Creates a new with exactly the same state as the current encoder. + /// + /// A new with identical state. + public IMimeEncoder Clone () + { + var encoder = new YEncoder (lineLength); + + encoder.crc = crc.Clone (); + encoder.octets = octets; + + return encoder; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return ContentEncoding.Default; } + } + + /// + /// Estimates the length of the output. + /// + /// + /// Estimates the number of bytes needed to encode the specified number of input bytes. + /// + /// The estimated output length. + /// The input length. + public int EstimateOutputLength (int inputLength) + { + return (inputLength * 2) + (inputLength / lineLength) + 1; + } + + void ValidateArguments (byte[] input, int startIndex, int length, byte[] output) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (output.Length < EstimateOutputLength (length)) + throw new ArgumentException ("The output buffer is not large enough to contain the encoded input.", nameof (output)); + } + + unsafe int Encode (byte* input, int length, byte* output) + { + byte* inend = input + length; + byte* outptr = output; + byte* inptr = input; + + while (inptr < inend) { + byte c = *inptr++; + + crc.Update (c); + + c += (byte) 42; + + if (c == 0 || c == (byte) '\t' || c == (byte) '\r' || c == (byte) '\n' || c == (byte) '=' || c == (byte) '.') { + *outptr++ = (byte) '='; + *outptr++ = (byte) (c + 64); + octets += 2; + } else { + *outptr++ = c; + octets++; + } + + if (octets >= lineLength) { + *outptr++ = (byte) '\n'; + octets = 0; + } + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer. + /// + /// + /// Encodes the specified input into the output buffer. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Encode (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Encode (inptr + startIndex, length, outptr); + } + } + } + + unsafe int Flush (byte* input, int length, byte* output) + { + byte* outptr = output; + + if (length > 0) + outptr += Encode (input, length, output); + + if (octets > 0) { + *outptr++ = (byte) '\n'; + octets = 0; + } + + return (int) (outptr - output); + } + + /// + /// Encodes the specified input into the output buffer, flushing any internal buffer state as well. + /// + /// + /// Encodes the specified input into the output buffer, flusing any internal state as well. + /// The output buffer should be large enough to hold all of the + /// encoded input. For estimating the size needed for the output buffer, + /// see . + /// + /// The number of bytes written to the output buffer. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer. + /// The output buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// is not large enough to contain the encoded content. + /// Use the method to properly determine the + /// necessary length of the byte array. + /// + public int Flush (byte[] input, int startIndex, int length, byte[] output) + { + ValidateArguments (input, startIndex, length, output); + + unsafe { + fixed (byte* inptr = input, outptr = output) { + return Flush (inptr + startIndex, length, outptr); + } + } + } + + /// + /// Resets the encoder. + /// + /// + /// Resets the state of the encoder. + /// + public void Reset () + { + crc.Reset (); + octets = 0; + } + } +} diff --git a/src/MimeKit/FormatOptions.cs b/src/MimeKit/FormatOptions.cs new file mode 100644 index 0000000..8866754 --- /dev/null +++ b/src/MimeKit/FormatOptions.cs @@ -0,0 +1,366 @@ +// +// FormatOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +using MimeKit.IO.Filters; + +namespace MimeKit { + /// + /// A New-Line format. + /// + /// + /// There are two commonly used line-endings used by modern Operating Systems. + /// Unix-based systems such as Linux and Mac OS use a single character ('\n' aka LF) + /// to represent the end of line where-as Windows (or DOS) uses a sequence of two + /// characters ("\r\n" aka CRLF). Most text-based network protocols such as SMTP, + /// POP3, and IMAP use the CRLF sequence as well. + /// + public enum NewLineFormat : byte { + /// + /// The Unix New-Line format ("\n"). + /// + Unix, + + /// + /// The DOS New-Line format ("\r\n"). + /// + Dos, + + /// + /// A mixed New-Line format where some lines use Unix-based line endings and + /// other lines use DOS-based line endings. + /// + Mixed, + } + + /// + /// Format options for serializing various MimeKit objects. + /// + /// + /// Represents the available options for formatting MIME messages + /// and entities when writing them to a stream. + /// + public class FormatOptions + { + static readonly byte[][] NewLineFormats = { + new byte[] { (byte) '\n' }, new byte[] { (byte) '\r', (byte) '\n' } + }; + + internal const int MaximumLineLength = 998; + internal const int MinimumLineLength = 60; + + const int DefaultMaxLineLength = 78; + + ParameterEncodingMethod parameterEncodingMethod; + bool allowMixedHeaderCharsets; + NewLineFormat newLineFormat; + bool verifyingSignature; + bool ensureNewLine; + bool international; + int maxLineLength; + + /// + /// The default formatting options. + /// + /// + /// If a custom is not passed to methods such as + /// , + /// the default options will be used. + /// + public static readonly FormatOptions Default; + + /// + /// Gets or sets the maximum line length used by the encoders. The encoders + /// use this value to determine where to place line breaks. + /// + /// + /// Specifies the maximum line length to use when line-wrapping headers. + /// + /// The maximum line length. + /// + /// is out of range. It must be between 60 and 998. + /// + /// + /// cannot be changed. + /// + public int MaxLineLength { + get { return maxLineLength; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + if (value < MinimumLineLength || value > MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (value)); + + maxLineLength = value; + } + } + + /// + /// Get or set the new-line format. + /// + /// + /// Specifies the new-line encoding to use when writing the message + /// or entity to a stream. + /// + /// The new-line format. + /// + /// is not a valid . + /// + /// + /// cannot be changed. + /// + public NewLineFormat NewLineFormat { + get { return newLineFormat; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + switch (newLineFormat) { + case NewLineFormat.Unix: + case NewLineFormat.Dos: + newLineFormat = value; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + } + } + + /// + /// Get or set whether the formatter should ensure that messages end with a new-line sequence. + /// + /// + /// By default, when writing a to a stream, the serializer attempts to + /// maintain byte-for-byte compatibility with the original stream that the message was parsed from. + /// This means that if the ogirinal message stream did not end with a new-line sequence, then the + /// output of writing the message back to a stream will also not end with a new-line sequence. + /// To override this behavior, you can set this property to true in order to ensure + /// that writing the message back to a stream will always end with a new-line sequence. + /// + /// true in order to ensure that the message will end with a new-line sequence; otherwise, false. + /// + /// cannot be changed. + /// + public bool EnsureNewLine { + get { return ensureNewLine; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + ensureNewLine = value; + } + } + + internal IMimeFilter CreateNewLineFilter (bool ensureNewLine = false) + { + switch (NewLineFormat) { + case NewLineFormat.Unix: + return new Dos2UnixFilter (ensureNewLine); + default: + return new Unix2DosFilter (ensureNewLine); + } + } + + internal string NewLine { + get { return NewLineFormat == NewLineFormat.Unix ? "\n" : "\r\n"; } + } + + internal byte[] NewLineBytes { + get { return NewLineFormats[(int) NewLineFormat]; } + } + + internal bool VerifyingSignature { + get { return verifyingSignature; } + set { verifyingSignature = value; } + } + + /// + /// Get the message headers that should be hidden. + /// + /// + /// Specifies the set of headers that should be removed when + /// writing a to a stream. + /// This is primarily meant for the purposes of removing Bcc + /// and Resent-Bcc headers when sending via a transport such as + /// SMTP. + /// + /// The message headers. + public HashSet HiddenHeaders { + get; private set; + } + + /// + /// Get or set whether the new "Internationalized Email" formatting standards should be used. + /// + /// + /// The new "Internationalized Email" format is defined by + /// rfc6530 and + /// rfc6532. + /// This feature should only be used when formatting messages meant to be sent via + /// SMTP using the SMTPUTF8 extension (rfc6531) + /// or when appending messages to an IMAP folder via UTF8 APPEND + /// (rfc6855). + /// + /// true if the new internationalized formatting should be used; otherwise, false. + /// + /// cannot be changed. + /// + public bool International { + get { return international; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + international = value; + } + } + + /// + /// Get or set whether the formatter should allow mixed charsets in the headers. + /// + /// + /// When this option is enabled, the MIME formatter will try to use us-ascii and/or + /// iso-8859-1 to encode headers when appropriate rather than being forced to use the + /// specified charset for all encoded-word tokens in order to maximize readability. + /// Unfortunately, mail clients like Outlook and Thunderbird do not treat + /// encoded-word tokens individually and assume that all tokens are encoded using the + /// charset declared in the first encoded-word token despite the specification + /// explicitly stating that each encoded-word token should be treated independently. + /// The Thunderbird bug can be tracked at + /// + /// https://bugzilla.mozilla.org/show_bug.cgi?id=317263. + /// + /// true if the formatter should be allowed to use us-ascii and/or iso-8859-1 when encoding headers; otherwise, false. + public bool AllowMixedHeaderCharsets { + get { return allowMixedHeaderCharsets; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + allowMixedHeaderCharsets = value; + } + } + + /// + /// Get or set the method to use for encoding Content-Type and Content-Disposition parameter values. + /// + /// + /// The method to use for encoding Content-Type and Content-Disposition parameter + /// values when the is set to + /// . + /// The MIME specifications specify that the proper method for encoding Content-Type + /// and Content-Disposition parameter values is the method described in + /// rfc2231. However, it is common for + /// some older email clients to improperly encode using the method described in + /// rfc2047 instead. + /// + /// The parameter encoding method that will be used. + /// + /// is not a valid value. + /// + public ParameterEncodingMethod ParameterEncodingMethod { + get { return parameterEncodingMethod; } + set { + if (this == Default) + throw new InvalidOperationException ("The default formatting options cannot be changed."); + + switch (value) { + case ParameterEncodingMethod.Rfc2047: + case ParameterEncodingMethod.Rfc2231: + parameterEncodingMethod = value; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + } + } + + static FormatOptions () + { + Default = new FormatOptions (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new set of formatting options for use with methods such as + /// . + /// + public FormatOptions () + { + HiddenHeaders = new HashSet (); + parameterEncodingMethod = ParameterEncodingMethod.Rfc2231; + maxLineLength = DefaultMaxLineLength; + allowMixedHeaderCharsets = false; + ensureNewLine = false; + international = false; + + if (Environment.NewLine.Length == 1) + newLineFormat = NewLineFormat.Unix; + else + newLineFormat = NewLineFormat.Dos; + } + + /// + /// Clones an instance of . + /// + /// + /// Clones the formatting options. + /// + /// An exact copy of the . + public FormatOptions Clone () + { + var options = new FormatOptions (); + options.maxLineLength = maxLineLength; + options.newLineFormat = newLineFormat; + options.ensureNewLine = ensureNewLine; + options.HiddenHeaders = new HashSet (HiddenHeaders); + options.allowMixedHeaderCharsets = allowMixedHeaderCharsets; + options.parameterEncodingMethod = parameterEncodingMethod; + options.verifyingSignature = verifyingSignature; + options.international = international; + return options; + } + + /// + /// Get the default formatting options in a thread-safe way. + /// + /// + /// Gets the default formatting options in a thread-safe way. + /// + /// The default formatting options. + internal static FormatOptions CloneDefault () + { + lock (Default) { + return Default.Clone (); + } + } + } +} diff --git a/src/MimeKit/GroupAddress.cs b/src/MimeKit/GroupAddress.cs new file mode 100644 index 0000000..3f29dc4 --- /dev/null +++ b/src/MimeKit/GroupAddress.cs @@ -0,0 +1,747 @@ +// +// GroupAddress.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// An address group, as specified by rfc0822. + /// + /// + /// Group addresses are rarely used anymore. Typically, if you see a group address, + /// it will be of the form: "undisclosed-recipients: ;". + /// + public class GroupAddress : InternetAddress + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name and list of addresses. The + /// specified text encoding is used when encoding the name according to the rules of rfc2047. + /// + /// The character encoding to be used for encoding the name. + /// The name of the group. + /// A list of addresses. + /// + /// is null. + /// + public GroupAddress (Encoding encoding, string name, IEnumerable addresses) : base (encoding, name) + { + Members = new InternetAddressList (addresses); + Members.Changed += MembersChanged; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name and list of addresses. + /// + /// The name of the group. + /// A list of addresses. + public GroupAddress (string name, IEnumerable addresses) : this (Encoding.UTF8, name, addresses) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name. The specified + /// text encoding is used when encoding the name according to the rules of rfc2047. + /// + /// The character encoding to be used for encoding the name. + /// The name of the group. + /// + /// is null. + /// + public GroupAddress (Encoding encoding, string name) : base (encoding, name) + { + Members = new InternetAddressList (); + Members.Changed += MembersChanged; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name. + /// + /// The name of the group. + public GroupAddress (string name) : this (Encoding.UTF8, name) + { + } + + /// + /// Clone the group address. + /// + /// + /// Clones the group address. + /// + /// The cloned group address. + public override InternetAddress Clone () + { + return new GroupAddress (Encoding, Name, Members.Select (x => x.Clone ())); + } + + /// + /// Gets the members of the group. + /// + /// + /// Represents the member addresses of the group. Typically the member addresses + /// will be of the variety, but it is possible + /// for groups to contain other groups. + /// + /// The list of members. + public InternetAddressList Members { + get; private set; + } + + internal override void Encode (FormatOptions options, StringBuilder builder, bool firstToken, ref int lineLength) + { + if (!string.IsNullOrEmpty (Name)) { + string name; + + if (!options.International) { + var encoded = Rfc2047.EncodePhrase (options, Encoding, Name); + name = Encoding.ASCII.GetString (encoded, 0, encoded.Length); + } else { + name = EncodeInternationalizedPhrase (Name); + } + + if (lineLength + name.Length > options.MaxLineLength) { + if (name.Length > options.MaxLineLength) { + // we need to break up the name... + builder.AppendFolded (options, firstToken, name, ref lineLength); + } else { + // the name itself is short enough to fit on a single line, + // but only if we write it on a line by itself + if (!firstToken && lineLength > 1) { + builder.LineWrap (options); + lineLength = 1; + } + + lineLength += name.Length; + builder.Append (name); + } + } else { + // we can safely fit the name on this line... + lineLength += name.Length; + builder.Append (name); + } + } + + builder.Append (": "); + lineLength += 2; + + Members.Encode (options, builder, false, ref lineLength); + + builder.Append (';'); + lineLength++; + } + + /// + /// Returns a string representation of the , + /// optionally encoding it for transport. + /// + /// + /// Returns a string containing the formatted group of addresses. If the + /// parameter is true, then the name of the group and all member addresses will be encoded + /// according to the rules defined in rfc2047, otherwise the names will not be encoded at all and + /// will therefor only be suitable for display purposes. + /// + /// A string representing the . + /// The formatting options. + /// If set to true, the will be encoded. + /// + /// is null. + /// + public override string ToString (FormatOptions options, bool encode) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + var builder = new StringBuilder (); + + if (encode) { + int lineLength = 0; + + Encode (options, builder, true, ref lineLength); + } else { + builder.Append (Name); + builder.Append (':'); + builder.Append (' '); + + for (int i = 0; i < Members.Count; i++) { + if (i > 0) + builder.Append (", "); + + builder.Append (Members[i]); + } + + builder.Append (';'); + } + + return builder.ToString (); + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Compares two group addresses to determine if they are identical or not. + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals (InternetAddress other) + { + var group = other as GroupAddress; + + if (group == null) + return false; + + return Name == group.Name && Members.Equals (group.Members); + } + + #endregion + + void MembersChanged (object sender, EventArgs e) + { + OnChanged (); + } + + static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out GroupAddress group) + { + var flags = AddressParserFlags.AllowGroupAddress; + InternetAddress address; + + if (throwOnError) + flags |= AddressParserFlags.ThrowOnError; + + if (!InternetAddress.TryParse (options, text, ref index, endIndex, 0, flags, out address)) { + group = null; + return false; + } + + group = (GroupAddress) address; + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed group address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out GroupAddress group) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, false, out group)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + group = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed group address. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out GroupAddress group) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out group); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed group address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out GroupAddress group) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, false, out group)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + group = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed group address. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out GroupAddress group) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out group); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The parsed group address. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out GroupAddress group) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, false, out group)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + group = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The parsed group address. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out GroupAddress group) + { + return TryParse (ParserOptions.Default, buffer, out group); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The text. + /// The parsed group address. + /// + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out GroupAddress group) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, false, out group)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + group = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The text. + /// The parsed group address. + /// + /// is null. + /// + public static bool TryParse (string text, out GroupAddress group) + { + return TryParse (ParserOptions.Default, text, out group); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + int index = startIndex; + GroupAddress group; + + if (!TryParse (options, buffer, ref index, endIndex, true, out group)) + throw new ParseException ("No group address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return group; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + int index = startIndex; + GroupAddress group; + + if (!TryParse (options, buffer, ref index, endIndex, true, out group)) + throw new ParseException ("No group address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return group; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + GroupAddress group; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, true, out group)) + throw new ParseException ("No group address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return group; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (ParserOptions options, string text) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + GroupAddress group; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, true, out group)) + throw new ParseException ("No group address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return group; + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a group address or + /// there is more than a single group address, then parsing will fail. + /// + /// The parsed . + /// The text. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static new GroupAddress Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + } +} diff --git a/src/MimeKit/Header.cs b/src/MimeKit/Header.cs new file mode 100644 index 0000000..24b6085 --- /dev/null +++ b/src/MimeKit/Header.cs @@ -0,0 +1,1582 @@ +// +// Header.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit.Utils; +using MimeKit.Cryptography; + +namespace MimeKit { + /// + /// A class representing a Message or MIME header. + /// + /// + /// Represents a single header field and value pair. + /// + public class Header + { + internal static readonly byte[] Colon = { (byte) ':' }; + internal readonly ParserOptions Options; + + // cached FormatOptions that change the way the header is formatted + //bool allowMixedHeaderCharsets = FormatOptions.Default.AllowMixedHeaderCharsets; + //NewLineFormat newLineFormat = FormatOptions.Default.NewLineFormat; + //bool international = FormatOptions.Default.International; + //Encoding charset = CharsetUtils.UTF8; + + readonly byte[] rawField; + bool explicitRawValue; + string textValue; + byte[] rawValue; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair. The encoding is used to determine which charset to use + /// when encoding the value according to the rules of rfc2047. + /// + /// The character encoding that should be used to + /// encode the header value. + /// The header identifier. + /// The value of the header. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid . + /// + public Header (Encoding encoding, HeaderId id, string value) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Options = ParserOptions.Default.Clone (); + Field = id.ToHeaderName (); + Id = id; + + rawField = Encoding.ASCII.GetBytes (Field); + SetValue (encoding, value); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair. The encoding is used to determine which charset to use + /// when encoding the value according to the rules of rfc2047. + /// + /// The charset that should be used to encode the + /// header value. + /// The header identifier. + /// The value of the header. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid . + /// + /// + /// is not supported. + /// + public Header (string charset, HeaderId id, string value) + { + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + var encoding = CharsetUtils.GetEncoding (charset); + Options = ParserOptions.Default.Clone (); + Field = id.ToHeaderName (); + Id = id; + + rawField = Encoding.ASCII.GetBytes (Field); + SetValue (encoding, value); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair with the UTF-8 encoding. + /// + /// The header identifier. + /// The value of the header. + /// + /// is null. + /// + /// + /// is not a valid . + /// + public Header (HeaderId id, string value) : this (Encoding.UTF8, id, value) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair. The encoding is used to determine which charset to use + /// when encoding the value according to the rules of rfc2047. + /// + /// The character encoding that should be used + /// to encode the header value. + /// The name of the header field. + /// The value of the header. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public Header (Encoding encoding, string field, string value) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (field == null) + throw new ArgumentNullException (nameof (field)); + + if (field.Length == 0) + throw new ArgumentException ("Header field names are not allowed to be empty.", nameof (field)); + + for (int i = 0; i < field.Length; i++) { + if (field[i] >= 127 || !IsAsciiAtom ((byte) field[i])) + throw new ArgumentException ("Illegal characters in header field name.", nameof (field)); + } + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Options = ParserOptions.Default.Clone (); + Id = field.ToHeaderId (); + Field = field; + + rawField = Encoding.ASCII.GetBytes (field); + SetValue (encoding, value); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair. The encoding is used to determine which charset to use + /// when encoding the value according to the rules of rfc2047. + /// + /// The charset that should be used to encode the + /// header value. + /// The name of the header field. + /// The value of the header. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + /// + /// is not supported. + /// + public Header (string charset, string field, string value) + { + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + if (field == null) + throw new ArgumentNullException (nameof (field)); + + if (field.Length == 0) + throw new ArgumentException ("Header field names are not allowed to be empty.", nameof (field)); + + for (int i = 0; i < field.Length; i++) { + if (field[i] >= 127 || !IsAsciiAtom ((byte) field[i])) + throw new ArgumentException ("Illegal characters in header field name.", nameof (field)); + } + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + var encoding = CharsetUtils.GetEncoding (charset); + Options = ParserOptions.Default.Clone (); + Id = field.ToHeaderId (); + Field = field; + + rawField = Encoding.ASCII.GetBytes (field); + SetValue (encoding, value); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header for the specified field and + /// value pair with the UTF-8 encoding. + /// + /// The name of the header field. + /// The value of the header. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public Header (string field, string value) : this (Encoding.UTF8, field, value) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header with the specified values. + /// This constructor is used by . + /// + /// The parser options used. + /// The id of the header. + /// The name of the header field. + /// The raw header field. + /// The raw value of the header. + protected Header (ParserOptions options, HeaderId id, string name, byte[] field, byte[] value) + { + Options = options; + rawField = field; + rawValue = value; + Field = name; + Id = id; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header with the specified raw values. + /// This constructor is used by the + /// TryParse methods. + /// + /// The parser options used. + /// The raw header field. + /// The raw value of the header. + /// true if the header field is invalid; othereise, false. + internal protected Header (ParserOptions options, byte[] field, byte[] value, bool invalid) + { + var chars = new char[field.Length]; + int count = 0; + + while (count < field.Length && (invalid || !field[count].IsBlank ())) { + chars[count] = (char) field[count]; + count++; + } + + Options = options; + rawField = field; + rawValue = value; + + Field = new string (chars, 0, count); + Id = Field.ToHeaderId (); + IsInvalid = invalid; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message or entity header with the specified raw values. + /// This constructor is used by and + /// when serializing new values for headers. + /// + /// The parser options used. + /// The id of the header. + /// The raw header field. + /// The raw value of the header. + internal protected Header (ParserOptions options, HeaderId id, string field, byte[] value) + { + Options = options; + rawField = Encoding.ASCII.GetBytes (field); + rawValue = value; + Field = field; + Id = id; + } + + /// + /// Clone the header. + /// + /// + /// Clones the header, copying the current RawValue. + /// + /// A copy of the header with its current state. + public Header Clone () + { + var header = new Header (Options, Id, Field, rawField, rawValue) { + explicitRawValue = explicitRawValue, + IsInvalid = IsInvalid + }; + + // if the textValue has already been calculated, set it on the cloned header as well. + header.textValue = textValue; + + return header; + } + + /// + /// Gets the stream offset of the beginning of the header. + /// + /// + /// If the offset is set, it refers to the byte offset where it + /// was found in the stream it was parsed from. + /// + /// The stream offset. + public long? Offset { + get; internal set; + } + + /// + /// Gets the name of the header field. + /// + /// + /// Represents the field name of the header. + /// + /// The name of the header field. + public string Field { + get; private set; + } + + /// + /// Gets the header identifier. + /// + /// + /// This property is mainly used for switch-statements for performance reasons. + /// + /// The header identifier. + public HeaderId Id { + get; private set; + } + + internal bool IsInvalid { + get; private set; + } + + /// + /// Gets the raw field name of the header. + /// + /// + /// Contains the raw field name of the header. + /// + /// The raw field name of the header. + public byte[] RawField { + get { return rawField; } + } + + /// + /// Gets the raw value of the header. + /// + /// + /// Contains the raw value of the header, before any decoding or charset conversion. + /// + /// The raw value of the header. + public byte[] RawValue { + get { return rawValue; } + } + + /// + /// Gets or sets the header value. + /// + /// + /// Represents the decoded header value and is suitable for displaying to the user. + /// + /// The header value. + /// + /// is null. + /// + public string Value { + get { + if (textValue == null) + textValue = Unfold (Rfc2047.DecodeText (Options, rawValue)); + + return textValue; + } + set { + SetValue (FormatOptions.Default, Encoding.UTF8, value); + } + } + + /// + /// Gets the header value using the specified character encoding. + /// + /// + /// If the raw header value does not properly encode non-ASCII text, the decoder + /// will fall back to a default charset encoding. Sometimes, however, this + /// default charset fallback is wrong and the mail client may wish to override + /// that default charset on a per-header basis. + /// By using this method, the client is able to override the fallback charset + /// on a per-header basis. + /// + /// The value. + /// The character encoding to use as a fallback. + public string GetValue (Encoding encoding) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + var options = Options.Clone (); + options.CharsetEncoding = encoding; + + return Unfold (Rfc2047.DecodeText (options, rawValue)); + } + + /// + /// Gets the header value using the specified charset. + /// + /// + /// If the raw header value does not properly encode non-ASCII text, the decoder + /// will fall back to a default charset encoding. Sometimes, however, this + /// default charset fallback is wrong and the mail client may wish to override + /// that default charset on a per-header basis. + /// By using this method, the client is able to override the fallback charset + /// on a per-header basis. + /// + /// The value. + /// The charset to use as a fallback. + public string GetValue (string charset) + { + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + var encoding = CharsetUtils.GetEncoding (charset); + + return GetValue (encoding); + } + + static byte[] EncodeAddressHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var encoded = new StringBuilder (" "); + int lineLength = field.Length + 2; + InternetAddressList list; + + if (!InternetAddressList.TryParse (options, value, out list)) + return (byte[]) format.NewLineBytes.Clone (); + + list.Encode (format, encoded, true, ref lineLength); + encoded.Append (format.NewLine); + + if (format.International) + return Encoding.UTF8.GetBytes (encoded.ToString ()); + + return Encoding.ASCII.GetBytes (encoded.ToString ()); + } + + static byte[] EncodeMessageIdHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + return encoding.GetBytes (" " + value + format.NewLine); + } + + delegate void ReceivedTokenSkipValueFunc (byte[] text, ref int index); + + static void ReceivedTokenSkipAtom (byte[] text, ref int index) + { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, text.Length, false) || index >= text.Length) + return; + + ParseUtils.SkipAtom (text, ref index, text.Length); + } + + static void ReceivedTokenSkipDomain (byte[] text, ref int index) + { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, text.Length, false)) + return; + + if (text[index] == (byte) '[') { + while (index < text.Length && text[index] != (byte) ']') + index++; + + if (index < text.Length) + index++; + + return; + } + + while (ParseUtils.SkipAtom (text, ref index, text.Length) && index < text.Length && text[index] == (byte) '.') + index++; + } + + static readonly byte[] ReceivedAddrSpecSentinels = { (byte) '>', (byte) ';' }; + static readonly byte[] ReceivedMessageIdSentinels = { (byte) '>' }; + + static void ReceivedTokenSkipAddress (byte[] text, ref int index) + { + string addrspec; + int at; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, text.Length, false) || index >= text.Length) + return; + + if (text[index] == (byte) '<') + index++; + + InternetAddress.TryParseAddrspec (text, ref index, text.Length, ReceivedAddrSpecSentinels, false, out addrspec, out at); + + if (index < text.Length && text[index] == (byte) '>') + index++; + } + + static void ReceivedTokenSkipMessageId (byte[] text, ref int index) + { + string addrspec; + int at; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, text.Length, false) || index >= text.Length) + return; + + if (text[index] == (byte) '<') { + index++; + + InternetAddress.TryParseAddrspec (text, ref index, text.Length, ReceivedMessageIdSentinels, false, out addrspec, out at); + + if (index < text.Length && text[index] == (byte) '>') + index++; + } else { + ParseUtils.SkipAtom (text, ref index, text.Length); + } + } + + struct ReceivedToken { + public readonly ReceivedTokenSkipValueFunc Skip; + public readonly string Atom; + + public ReceivedToken (string atom, ReceivedTokenSkipValueFunc skip) + { + Atom = atom; + Skip = skip; + } + } + + static readonly ReceivedToken[] ReceivedTokens = { + new ReceivedToken ("from", ReceivedTokenSkipDomain), + new ReceivedToken ("by", ReceivedTokenSkipDomain), + new ReceivedToken ("via", ReceivedTokenSkipDomain), + new ReceivedToken ("with", ReceivedTokenSkipAtom), + new ReceivedToken ("id", ReceivedTokenSkipMessageId), + new ReceivedToken ("for", ReceivedTokenSkipAddress), + }; + + class ReceivedTokenValue + { + public readonly int StartIndex; + public readonly int Length; + + public ReceivedTokenValue (int startIndex, int length) + { + StartIndex = startIndex; + Length = length; + } + } + + static byte[] EncodeReceivedHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var tokens = new List (); + var rawValue = encoding.GetBytes (value); + var encoded = new StringBuilder (); + int lineLength = field.Length + 1; + bool date = false; + int index = 0; + int count = 0; + + while (index < rawValue.Length) { + ReceivedTokenValue token = null; + int startIndex = index; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (rawValue, ref index, rawValue.Length, false) || index >= rawValue.Length) { + tokens.Add (new ReceivedTokenValue (startIndex, index - startIndex)); + break; + } + + while (index < rawValue.Length && !rawValue[index].IsWhitespace ()) + index++; + + var atom = encoding.GetString (rawValue, startIndex, index - startIndex); + + for (int i = 0; i < ReceivedTokens.Length; i++) { + if (atom == ReceivedTokens[i].Atom) { + ReceivedTokens[i].Skip (rawValue, ref index); + + if (ParseUtils.SkipCommentsAndWhiteSpace (rawValue, ref index, rawValue.Length, false)) { + if (index < rawValue.Length && rawValue[index] == (byte) ';') { + date = true; + index++; + } + } + + token = new ReceivedTokenValue (startIndex, index - startIndex); + break; + } + } + + if (token == null) { + if (ParseUtils.SkipCommentsAndWhiteSpace (rawValue, ref index, rawValue.Length, false)) { + while (index < rawValue.Length && !rawValue[index].IsWhitespace ()) + index++; + } + + token = new ReceivedTokenValue (startIndex, index - startIndex); + } + + tokens.Add (token); + + ParseUtils.SkipWhiteSpace (rawValue, ref index, rawValue.Length); + + if (date && index < rawValue.Length) { + // slurp up the date (the final token) + tokens.Add (new ReceivedTokenValue (index, rawValue.Length - index)); + break; + } + } + + foreach (var token in tokens) { + var text = encoding.GetString (rawValue, token.StartIndex, token.Length).TrimEnd (); + + if (count > 0 && lineLength + text.Length + 1 > format.MaxLineLength) { + encoded.Append (format.NewLine); + encoded.Append ('\t'); + lineLength = 1; + count = 0; + } else { + encoded.Append (' '); + lineLength++; + } + + lineLength += text.Length; + encoded.Append (text); + count++; + } + + encoded.Append (format.NewLine); + + return encoding.GetBytes (encoded.ToString ()); + } + + static byte[] EncodeAuthenticationResultsHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var buffer = Encoding.UTF8.GetBytes (value); + + if (!AuthenticationResults.TryParse (buffer, out AuthenticationResults authres)) + return EncodeUnstructuredHeader (options, format, encoding, field, value); + + var encoded = new StringBuilder (); + int lineLength = field.Length + 1; + + authres.Encode (format, encoded, lineLength); + + return encoding.GetBytes (encoded.ToString ()); + } + + static void EncodeDkimLongValue (FormatOptions format, StringBuilder encoded, ref int lineLength, string value) + { + int startIndex = 0; + + do { + int lineLeft = format.MaxLineLength - lineLength; + int index = Math.Min (startIndex + lineLeft, value.Length); + + encoded.Append (value.Substring (startIndex, index - startIndex)); + lineLength += (index - startIndex); + + if (index == value.Length) + break; + + encoded.Append (format.NewLine); + encoded.Append ('\t'); + lineLength = 1; + + startIndex = index; + } while (true); + } + + static void EncodeDkimHeaderList (FormatOptions format, StringBuilder encoded, ref int lineLength, string value, char delim) + { + var tokens = value.Split (delim); + + for (int i = 0; i < tokens.Length; i++) { + if (i > 0) { + encoded.Append (delim); + lineLength++; + } + + if (lineLength + tokens[i].Length + 1 > format.MaxLineLength) { + encoded.Append (format.NewLine); + encoded.Append ('\t'); + lineLength = 1; + + if (tokens[i].Length + 1 > format.MaxLineLength) { + EncodeDkimLongValue (format, encoded, ref lineLength, tokens[i]); + } else { + lineLength += tokens[i].Length; + encoded.Append (tokens[i]); + } + } else { + lineLength += tokens[i].Length; + encoded.Append (tokens[i]); + } + } + } + + static byte[] EncodeDkimOrArcSignatureHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var encoded = new StringBuilder (); + int lineLength = field.Length + 1; + var token = new StringBuilder (); + int index = 0; + + while (index < value.Length) { + while (index < value.Length && IsWhiteSpace (value[index])) + index++; + + int startIndex = index; + string name; + + while (index < value.Length && value[index] != '=') { + if (!IsWhiteSpace (value[index])) + token.Append (value[index]); + index++; + } + + name = value.Substring (startIndex, index - startIndex); + + while (index < value.Length && value[index] != ';') { + if (!IsWhiteSpace (value[index])) + token.Append (value[index]); + index++; + } + + if (index < value.Length && value[index] == ';') { + token.Append (';'); + index++; + } + + if (lineLength + token.Length + 1 > format.MaxLineLength || name == "bh" || name == "b") { + encoded.Append (format.NewLine); + encoded.Append ('\t'); + lineLength = 1; + } else { + encoded.Append (' '); + lineLength++; + } + + if (token.Length > format.MaxLineLength) { + switch (name) { + case "z": + EncodeDkimHeaderList (format, encoded, ref lineLength, token.ToString (), '|'); + break; + case "h": + EncodeDkimHeaderList (format, encoded, ref lineLength, token.ToString (), ':'); + break; + default: + EncodeDkimLongValue (format, encoded, ref lineLength, token.ToString ()); + break; + } + } else { + encoded.Append (token.ToString ()); + lineLength += token.Length; + } + + token.Length = 0; + } + + encoded.Append (format.NewLine); + + return encoding.GetBytes (encoded.ToString ()); + } + + static byte[] EncodeReferencesHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var encoded = new StringBuilder (); + int lineLength = field.Length + 1; + int count = 0; + + foreach (var reference in MimeUtils.EnumerateReferences (value)) { + if (count > 0 && lineLength + reference.Length + 3 > format.MaxLineLength) { + encoded.Append (format.NewLine); + encoded.Append ('\t'); + lineLength = 1; + count = 0; + } else { + encoded.Append (' '); + lineLength++; + } + + encoded.Append ('<').Append (reference).Append ('>'); + lineLength += reference.Length + 2; + count++; + } + + encoded.Append (format.NewLine); + + return encoding.GetBytes (encoded.ToString ()); + } + + static bool IsWhiteSpace (char c) + { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; + } + + static IEnumerable TokenizeText (string text) + { + int index = 0; + + while (index < text.Length) { + int startIndex = index; + + while (index < text.Length && !IsWhiteSpace (text[index])) + index++; + + yield return text.Substring (startIndex, index - startIndex); + + if (index == text.Length) + break; + + startIndex = index; + + while (index < text.Length && IsWhiteSpace (text[index])) + index++; + + yield return text.Substring (startIndex, index - startIndex); + } + + yield break; + } + + class BrokenWord + { + public readonly char[] Text; + public readonly int StartIndex; + public readonly int Length; + + public BrokenWord (char[] text, int startIndex, int length) + { + StartIndex = startIndex; + Length = length; + Text = text; + } + } + + static IEnumerable WordBreak (FormatOptions format, string word, int lineLength) + { + var chars = word.ToCharArray (); + int startIndex = 0; + + lineLength = Math.Max (lineLength, 1); + + while (startIndex < word.Length) { + int length = Math.Min (format.MaxLineLength - lineLength, word.Length - startIndex); + + if (char.IsSurrogatePair (word, startIndex + length - 1)) + length--; + + yield return new BrokenWord (chars, startIndex, length); + + startIndex += length; + lineLength = 1; + } + + yield break; + } + + internal static string Fold (FormatOptions format, string field, string value) + { + var folded = new StringBuilder (value.Length); + int lineLength = field.Length + 2; + int lastLwsp = -1; + + folded.Append (' '); + + var words = TokenizeText (value); + + foreach (var word in words) { + if (IsWhiteSpace (word[0])) { + if (lineLength + word.Length > format.MaxLineLength) { + for (int i = 0; i < word.Length; i++) { + if (lineLength > format.MaxLineLength) { + folded.Append (format.NewLine); + lineLength = 0; + } + + folded.Append (word[i]); + lineLength++; + } + } else { + lineLength += word.Length; + folded.Append (word); + } + + lastLwsp = folded.Length - 1; + continue; + } + + if (lastLwsp != -1 && lineLength + word.Length > format.MaxLineLength) { + folded.Insert (lastLwsp, format.NewLine); + lineLength = 1; + lastLwsp = -1; + } + + if (word.Length > format.MaxLineLength) { + foreach (var broken in WordBreak (format, word, lineLength)) { + if (lineLength + broken.Length > format.MaxLineLength) { + folded.Append (format.NewLine); + folded.Append (' '); + lineLength = 1; + } + + folded.Append (broken.Text, broken.StartIndex, broken.Length); + lineLength += broken.Length; + } + } else { + lineLength += word.Length; + folded.Append (word); + } + } + + folded.Append (format.NewLine); + + return folded.ToString (); + } + + static byte[] EncodeContentDisposition (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var disposition = ContentDisposition.Parse (options, value); + var encoded = disposition.Encode (format, encoding); + + return Encoding.UTF8.GetBytes (encoded); + } + + static byte[] EncodeContentType (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + var contentType = ContentType.Parse (options, value); + var encoded = contentType.Encode (format, encoding); + + return Encoding.UTF8.GetBytes (encoded); + } + + static byte[] EncodeUnstructuredHeader (ParserOptions options, FormatOptions format, Encoding encoding, string field, string value) + { + if (format.International) { + var folded = Fold (format, field, value); + + return Encoding.UTF8.GetBytes (folded); + } + + var encoded = Rfc2047.EncodeText (format, encoding, value); + + return Rfc2047.FoldUnstructuredHeader (format, field, encoded); + } + + /// + /// Format the raw value of the header to conform with the specified formatting options. + /// + /// + /// This method will called by the SetValue + /// methods and may also be conditionally called when the header is being written to a + /// . + /// + /// The formatting options. + /// The character encoding to be used. + /// The decoded (and unfolded) header value. + /// A byte array containing the raw header value that should be written. + protected virtual byte[] FormatRawValue (FormatOptions format, Encoding encoding, string value) + { + switch (Id) { + case HeaderId.DispositionNotificationTo: + case HeaderId.ResentReplyTo: + case HeaderId.ResentSender: + case HeaderId.ResentFrom: + case HeaderId.ResentBcc: + case HeaderId.ResentCc: + case HeaderId.ResentTo: + case HeaderId.ReplyTo: + case HeaderId.Sender: + case HeaderId.From: + case HeaderId.Bcc: + case HeaderId.Cc: + case HeaderId.To: + return EncodeAddressHeader (Options, format, encoding, Field, value); + case HeaderId.Received: + return EncodeReceivedHeader (Options, format, encoding, Field, value); + case HeaderId.ResentMessageId: + case HeaderId.InReplyTo: + case HeaderId.MessageId: + case HeaderId.ContentId: + return EncodeMessageIdHeader (Options, format, encoding, Field, value); + case HeaderId.References: + return EncodeReferencesHeader (Options, format, encoding, Field, value); + case HeaderId.ContentDisposition: + return EncodeContentDisposition (Options, format, encoding, Field, value); + case HeaderId.ContentType: + return EncodeContentType (Options, format, encoding, Field, value); + case HeaderId.ArcAuthenticationResults: + case HeaderId.AuthenticationResults: + return EncodeAuthenticationResultsHeader (Options, format, encoding, Field, value); + case HeaderId.ArcMessageSignature: + case HeaderId.ArcSeal: + case HeaderId.DkimSignature: + return EncodeDkimOrArcSignatureHeader (Options, format, encoding, Field, value); + default: + return EncodeUnstructuredHeader (Options, format, encoding, Field, value); + } + } + + internal byte[] GetRawValue (FormatOptions format) + { + if (format.International && !explicitRawValue) { + if (textValue == null) + textValue = Unfold (Rfc2047.DecodeText (Options, rawValue)); + + // Note: if we're reformatting to be International, then charset doesn't matter. + return FormatRawValue (format, CharsetUtils.UTF8, textValue); + } + + return rawValue; + } + + /// + /// Sets the header value using the specified formatting options and character encoding. + /// + /// + /// When a particular charset is desired for encoding the header value + /// according to the rules of rfc2047, this method should be used + /// instead of the setter. + /// + /// The formatting options. + /// A character encoding. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public void SetValue (FormatOptions format, Encoding encoding, string value) + { + if (format == null) + throw new ArgumentNullException (nameof (format)); + + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + textValue = Unfold (value.Trim ()); + + rawValue = FormatRawValue (format, encoding, textValue); + + // cache the formatting options that change the way the header is formatted + //allowMixedHeaderCharsets = format.AllowMixedHeaderCharsets; + //newLineFormat = format.NewLineFormat; + //international = format.International; + //charset = encoding; + + OnChanged (); + } + + /// + /// Sets the header value using the specified character encoding. + /// + /// + /// When a particular charset is desired for encoding the header value + /// according to the rules of rfc2047, this method should be used + /// instead of the setter. + /// + /// A character encoding. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + public void SetValue (Encoding encoding, string value) + { + SetValue (FormatOptions.Default, encoding, value); + } + + /// + /// Sets the header value using the specified formatting options and charset. + /// + /// + /// When a particular charset is desired for encoding the header value + /// according to the rules of rfc2047, this method should be used + /// instead of the setter. + /// + /// The formatting options. + /// A charset encoding. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not supported. + /// + public void SetValue (FormatOptions format, string charset, string value) + { + if (format == null) + throw new ArgumentNullException (nameof (format)); + + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + var encoding = CharsetUtils.GetEncoding (charset); + + SetValue (format, encoding, value); + } + + /// + /// Sets the header value using the specified charset. + /// + /// + /// When a particular charset is desired for encoding the header value + /// according to the rules of rfc2047, this method should be used + /// instead of the setter. + /// + /// A charset encoding. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not supported. + /// + public void SetValue (string charset, string value) + { + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + var encoding = CharsetUtils.GetEncoding (charset); + + SetValue (FormatOptions.Default, encoding, value); + } + + /// + /// Set the raw header value. + /// + /// + /// Sets the raw header value. + /// This method can be used to override default encoding and folding behavior + /// for a particular header. + /// + /// The raw header value. + /// + /// is null. + /// + /// + /// does not end with a new-line character. + /// + public void SetRawValue (byte[] value) + { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value.Length == 0 || value[value.Length - 1] != (byte) '\n') + throw new ArgumentException ("The raw value MUST end with a new-line character.", nameof (value)); + + explicitRawValue = true; + rawValue = value; + textValue = null; + + OnChanged (); + } + + internal event EventHandler Changed; + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + /// + /// Returns a string representation of the header. + /// + /// + /// Formats the header field and value in a way that is suitable for display. + /// + /// A string representing the . + public override string ToString () + { + return IsInvalid ? Field : Field + ": " + Value; + } + + /// + /// Unfold the specified header value. + /// + /// + /// Unfolds the header value so that it becomes suitable for display. + /// Since is already unfolded, this method is really + /// only needed when working with raw header strings. + /// + /// The unfolded header value. + /// The header text. + public static unsafe string Unfold (string text) + { + int startIndex; + int endIndex; + int i = 0; + + if (text == null) + return string.Empty; + + while (i < text.Length && char.IsWhiteSpace (text[i])) + i++; + + if (i == text.Length) + return string.Empty; + + startIndex = i; + endIndex = i; + + while (i < text.Length) { + if (!char.IsWhiteSpace (text[i++])) + endIndex = i; + } + + int count = endIndex - startIndex; + char[] chars = new char[count]; + + fixed (char* outbuf = chars) { + char* outptr = outbuf; + + for (i = startIndex; i < endIndex; i++) { + if (text[i] != '\r' && text[i] != '\n') + *outptr++ = text[i]; + } + + count = (int) (outptr - outbuf); + } + + return new string (chars, 0, count); + } + + static bool IsAsciiAtom (byte c) + { + return c.IsAsciiAtom (); + } + + static bool IsControl (byte c) + { + return c.IsCtrl (); + } + + static bool IsBlank (byte c) + { + return c.IsBlank (); + } + + internal static unsafe bool TryParse (ParserOptions options, byte* input, int length, bool strict, out Header header) + { + byte* inend = input + length; + byte* start = input; + byte* inptr = input; + var invalid = false; + + // find the end of the field name + if (strict) { + while (inptr < inend && IsAsciiAtom (*inptr)) + inptr++; + } else { + while (inptr < inend && *inptr != (byte) ':' && !IsControl (*inptr)) + inptr++; + } + + while (inptr < inend && IsBlank (*inptr)) + inptr++; + + if (inptr == inend || *inptr != ':') { + if (strict) { + header = null; + return false; + } + + invalid = true; + inptr = inend; + } + + var field = new byte[(int) (inptr - start)]; + fixed (byte* outbuf = field) { + byte* outptr = outbuf; + + while (start < inptr) + *outptr++ = *start++; + } + + byte[] value; + + if (inptr < inend) { + inptr++; + + int count = (int) (inend - inptr); + value = new byte[count]; + + fixed (byte* outbuf = value) { + byte* outptr = outbuf; + + while (inptr < inend) + *outptr++ = *inptr++; + } + } else { + value = new byte[0]; + } + + header = new Header (options, field, value, invalid); + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed header. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out Header header) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + unsafe { + fixed (byte* inptr = buffer) { + return TryParse (options.Clone (), inptr + startIndex, length, true, out header); + } + } + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed header. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out Header header) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out header); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the supplied buffer starting at the specified index. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed header. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out Header header) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int length = buffer.Length - startIndex; + + unsafe { + fixed (byte* inptr = buffer) { + return TryParse (options.Clone (), inptr + startIndex, length, true, out header); + } + } + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the supplied buffer starting at the specified index. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed header. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out Header header) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out header); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the specified buffer. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The parsed header. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out Header header) + { + return TryParse (options, buffer, 0, out header); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a header from the specified buffer. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The input buffer. + /// The parsed header. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out Header header) + { + return TryParse (ParserOptions.Default, buffer, out header); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a header from the specified text. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The parser options to use. + /// The text to parse. + /// The parsed header. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out Header header) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + + unsafe { + fixed (byte *inptr = buffer) { + return TryParse (options.Clone (), inptr, buffer.Length, true, out header); + } + } + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a header from the specified text. + /// + /// true, if the header was successfully parsed, false otherwise. + /// The text to parse. + /// The parsed header. + /// + /// is null. + /// + public static bool TryParse (string text, out Header header) + { + return TryParse (ParserOptions.Default, text, out header); + } + } +} diff --git a/src/MimeKit/HeaderId.cs b/src/MimeKit/HeaderId.cs new file mode 100644 index 0000000..33f8a8c --- /dev/null +++ b/src/MimeKit/HeaderId.cs @@ -0,0 +1,792 @@ +// +// HeaderId.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// An enumeration of common header fields. + /// + /// + /// Comparing enum values is not only faster, but less error prone than + /// comparing strings. + /// + public enum HeaderId { + /// + /// The Accept-Language header field. + /// + AcceptLanguage, + + /// + /// The Ad-Hoc header field. + /// + AdHoc, + + /// + /// The Alternate-Recipient header field. + /// + AlternateRecipient, + + /// + /// The Apparently-To header field. + /// + ApparentlyTo, + + /// + /// The Approved header field. + /// + Approved, + + /// + /// The ARC-Authentication-Results header field. + /// + [HeaderName ("ARC-Authentication-Results")] + ArcAuthenticationResults, + + /// + /// The ARC-Message-Signature header field. + /// + [HeaderName ("ARC-Message-Signature")] + ArcMessageSignature, + + /// + /// The ARC-Seal header field. + /// + [HeaderName ("ARC-Seal")] + ArcSeal, + + /// + /// The Archive header field. + /// + Archive, + + /// + /// The Archived-At header field. + /// + ArchivedAt, + + /// + /// The Article header field. + /// + Article, + + /// + /// The Authentication-Results header field. + /// + AuthenticationResults, + + /// + /// The Autocrypt header field. + /// + Autocrypt, + + /// + /// The Autocrypt-Gossip header field. + /// + AutocryptGossip, + + /// + /// The Autocrypt-Setup-Message header field. + /// + AutocryptSetupMessage, + + /// + /// The Autoforwarded header field. + /// + Autoforwarded, + + /// + /// The Auto-Submitted header field. + /// + AutoSubmitted, + + /// + /// The Autosubmitted header field. + /// + Autosubmitted, + + /// + /// The Base header field. + /// + Base, + + /// + /// The Bcc header field. + /// + Bcc, + + /// + /// The Body header field. + /// + Body, + + /// + /// The Bytes header field. + /// + Bytes, + + /// + /// The Cc header field. + /// + Cc, + + /// + /// The Comments header field. + /// + Comments, + + /// + /// The Content-Alternative header field. + /// + ContentAlternative, + + /// + /// The Content-Base header field. + /// + ContentBase, + + /// + /// The Content-Class header field. + /// + ContentClass, + + /// + /// The Content-Description header field. + /// + ContentDescription, + + /// + /// The Content-Disposition header field. + /// + ContentDisposition, + + /// + /// The Content-Duration header field. + /// + ContentDuration, + + /// + /// The Content-Features header field. + /// + ContentFeatures, + + /// + /// The Content-Id header field. + /// + ContentId, + + /// + /// The Content-Identifier header field. + /// + ContentIdentifier, + + /// + /// The Content-Language header field. + /// + ContentLanguage, + + /// + /// The Content-Length header field. + /// + ContentLength, + + /// + /// The Content-Location header field. + /// + ContentLocation, + + /// + /// The Content-Md5 header field. + /// + ContentMd5, + + /// + /// The Content-Return header field. + /// + ContentReturn, + + /// + /// The Content-Transfer-Encoding header field. + /// + ContentTransferEncoding, + + /// + /// The Content-Translation-Type header field. + /// + ContentTranslationType, + + /// + /// The Content-Type header field. + /// + ContentType, + + /// + /// The Control header field. + /// + Control, + + /// + /// The Conversion header field. + /// + Conversion, + + /// + /// The Conversion-With-Loss header field. + /// + ConversionWithLoss, + + /// + /// The Date header field. + /// + Date, + + /// + /// The Date-Received header field. + /// + DateReceived, + + /// + /// The Deferred-Delivery header field. + /// + DeferredDelivery, + + /// + /// The Delivery-Date header field. + /// + DeliveryDate, + + /// + /// The Disclose-Recipients header field. + /// + DiscloseRecipients, + + /// + /// The Disposition-Notification-Options header field. + /// + DispositionNotificationOptions, + + /// + /// The Disposition-Notification-To header field. + /// + DispositionNotificationTo, + + /// + /// The Distribution header field. + /// + Distribution, + + /// + /// The DKIM-Signature header field. + /// + [HeaderName ("DKIM-Signature")] + DkimSignature, + + /// + /// The DomainKey-Signature header field. + /// + [HeaderName ("DomainKey-Signature")] + DomainKeySignature, + + /// + /// The Encoding header field. + /// + Encoding, + + /// + /// The Encrypted header field. + /// + Encrypted, + + /// + /// The Expires header field. + /// + Expires, + + /// + /// The Expiry-Date header field. + /// + ExpiryDate, + + /// + /// The Followup-To header field. + /// + FollowupTo, + + /// + /// The From header field. + /// + From, + + /// + /// The Generate-Delivery-Report header field. + /// + GenerateDeliveryReport, + + /// + /// The Importance header field. + /// + Importance, + + /// + /// The Injection-Date header field. + /// + InjectionDate, + + /// + /// The Injection-Info header field. + /// + InjectionInfo, + + /// + /// The In-Reply-To header field. + /// + InReplyTo, + + /// + /// The Keywords header field. + /// + Keywords, + + /// + /// The Language header. + /// + Language, + + /// + /// The Latest-Delivery-Time header. + /// + LatestDeliveryTime, + + /// + /// The Lines header field. + /// + Lines, + + /// + /// THe List-Archive header field. + /// + ListArchive, + + /// + /// The List-Help header field. + /// + ListHelp, + + /// + /// The List-Id header field. + /// + ListId, + + /// + /// The List-Owner header field. + /// + ListOwner, + + /// + /// The List-Post header field. + /// + ListPost, + + /// + /// The List-Subscribe header field. + /// + ListSubscribe, + + /// + /// The List-Unsubscribe header field. + /// + ListUnsubscribe, + + /// + /// The List-Unsubscribe-Post header field. + /// + ListUnsubscribePost, + + /// + /// The Message-Id header field. + /// + MessageId, + + /// + /// The MIME-Version header field. + /// + [HeaderName ("MIME-Version")] + MimeVersion, + + /// + /// The Newsgroups header field. + /// + Newsgroups, + + /// + /// The Nntp-Posting-Host header field. + /// + NntpPostingHost, + + /// + /// The Organization header field. + /// + Organization, + + /// + /// The Original-From header field. + /// + OriginalFrom, + + /// + /// The Original-Message-Id header field. + /// + OriginalMessageId, + + /// + /// The Original-Recipient header field. + /// + OriginalRecipient, + + /// + /// The Original-Return-Address header field. + /// + OriginalReturnAddress, + + /// + /// The Original-Subject header field. + /// + OriginalSubject, + + /// + /// The Path header field. + /// + Path, + + /// + /// The Precedence header field. + /// + Precedence, + + /// + /// The Prevent-NonDelivery-Report header field. + /// + [HeaderName ("Prevent-NonDelivery-Report")] + PreventNonDeliveryReport, + + /// + /// The Priority header field. + /// + Priority, + + /// + /// The Received header field. + /// + Received, + + /// + /// The Received-SPF header field. + /// + [HeaderName ("Received-SPF")] + ReceivedSPF, + + /// + /// The References header field. + /// + References, + + /// + /// The Relay-Version header field. + /// + RelayVersion, + + /// + /// The Reply-By header field. + /// + ReplyBy, + + /// + /// The Reply-To header field. + /// + ReplyTo, + + /// + /// The Require-Recipient-Valid-Since header field. + /// + RequireRecipientValidSince, + + /// + /// The Resent-Bcc header field. + /// + ResentBcc, + + /// + /// The Resent-Cc header field. + /// + ResentCc, + + /// + /// The Resent-Date header field. + /// + ResentDate, + + /// + /// The Resent-From header field. + /// + ResentFrom, + + /// + /// The Resent-Message-Id header field. + /// + ResentMessageId, + + /// + /// The Resent-Reply-To header field. + /// + ResentReplyTo, + + /// + /// The Resent-Sender header field. + /// + ResentSender, + + /// + /// The Resent-To header field. + /// + ResentTo, + + /// + /// The Return-Path header field. + /// + ReturnPath, + + /// + /// The Return-Receipt-To header field. + /// + ReturnReceiptTo, + + /// + /// The See-Also header field. + /// + SeeAlso, + + /// + /// The Sender header field. + /// + Sender, + + /// + /// The Sensitivity header field. + /// + Sensitivity, + + /// + /// The Solicitation header field. + /// + Solicitation, + + /// + /// The Status header field. + /// + Status, + + /// + /// The Subject header field. + /// + Subject, + + /// + /// The Summary header field. + /// + Summary, + + /// + /// The Supersedes header field. + /// + Supersedes, + + /// + /// The To header field. + /// + To, + + /// + /// The User-Agent header field. + /// + UserAgent, + + /// + /// The X400-Content-Identifier header field. + /// + [HeaderName ("X400-Content-Identifier")] + X400ContentIdentifier, + + /// + /// The X400-Content-Return header field. + /// + [HeaderName ("X400-Content-Return")] + X400ContentReturn, + + /// + /// The X400-Content-Type header field. + /// + [HeaderName ("X400-Content-Type")] + X400ContentType, + + /// + /// The X400-MTS-Identifier header field. + /// + [HeaderName ("X400-MTS-Identifier")] + X400MTSIdentifier, + + /// + /// The X400-Originator header field. + /// + [HeaderName ("X400-Originator")] + X400Originator, + + /// + /// The X400-Received header field. + /// + [HeaderName ("X400-Received")] + X400Received, + + /// + /// The X400-Recipients header field. + /// + [HeaderName ("X400-Recipients")] + X400Recipients, + + /// + /// The X400-Trace header field. + /// + [HeaderName ("X400-Trace")] + X400Trace, + + /// + /// The X-Mailer header field. + /// + XMailer, + + /// + /// The X-MSMail-Priority header field. + /// + [HeaderName ("X-MSMail-Priority")] + XMSMailPriority, + + /// + /// The X-Priority header field. + /// + XPriority, + + /// + /// The X-Status header field. + /// + XStatus, + + /// + /// An unknown header field. + /// + Unknown = -1 + } + + [AttributeUsage (AttributeTargets.Field)] + class HeaderNameAttribute : Attribute { + public HeaderNameAttribute (string name) + { + HeaderName = name; + } + + public string HeaderName { + get; protected set; + } + } + + /// + /// extension methods. + /// + /// + /// extension methods. + /// + public static class HeaderIdExtensions + { + static readonly Dictionary dict; + + static HeaderIdExtensions () + { + var values = (HeaderId[]) Enum.GetValues (typeof (HeaderId)); + + dict = new Dictionary (values.Length - 1, MimeUtils.OrdinalIgnoreCase); + + for (int i = 0; i < values.Length - 1; i++) + dict.Add (values[i].ToHeaderName (), values[i]); + } + + /// + /// Converts the enum value into the equivalent header field name. + /// + /// + /// Converts the enum value into the equivalent header field name. + /// + /// The header name. + /// The enum value. + public static string ToHeaderName (this HeaderId value) + { + var name = value.ToString (); + +#if NETSTANDARD1_3 || NETSTANDARD1_6 + var field = typeof (HeaderId).GetTypeInfo ().GetDeclaredField (name); + var attrs = field.GetCustomAttributes (typeof (HeaderNameAttribute), false).ToArray (); +#else + var field = typeof (HeaderId).GetField (name); + var attrs = field.GetCustomAttributes (typeof (HeaderNameAttribute), false); +#endif + + if (attrs != null && attrs.Length == 1) + return ((HeaderNameAttribute) attrs[0]).HeaderName; + + var builder = new StringBuilder (name); + + for (int i = 1; i < builder.Length; i++) { + if (char.IsUpper (builder[i])) + builder.Insert (i++, '-'); + } + + return builder.ToString (); + } + + internal static HeaderId ToHeaderId (this string name) + { + HeaderId value; + + if (!dict.TryGetValue (name, out value)) + return HeaderId.Unknown; + + return value; + } + } +} diff --git a/src/MimeKit/HeaderList.cs b/src/MimeKit/HeaderList.cs new file mode 100644 index 0000000..0317d41 --- /dev/null +++ b/src/MimeKit/HeaderList.cs @@ -0,0 +1,1547 @@ +// +// HeaderList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A list of s. + /// + /// + /// Represents a list of headers as found in a + /// or . + /// + public sealed class HeaderList : IList
+ { + internal readonly ParserOptions Options; + + // this table references the first header of each field + readonly Dictionary table; + readonly List
headers; + + internal HeaderList (ParserOptions options) + { + table = new Dictionary (MimeUtils.OrdinalIgnoreCase); + headers = new List
(); + Options = options; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new empty header list. + /// + public HeaderList () : this (ParserOptions.Default.Clone ()) + { + } + + /// + /// Add a header with the specified field and value. + /// + /// + /// Adds a new header for the specified field and value pair. + /// + /// The header identifier. + /// The header value. + /// + /// is null. + /// + /// + /// is not a valid . + /// + public void Add (HeaderId id, string value) + { + Add (id, Encoding.UTF8, value); + } + + /// + /// Add a header with the specified field and value. + /// + /// + /// Adds a new header for the specified field and value pair. + /// + /// The name of the header field. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public void Add (string field, string value) + { + Add (field, Encoding.UTF8, value); + } + + /// + /// Add a header with the specified field and value. + /// + /// + /// Adds a new header for the specified field and value pair. + /// + /// The header identifier. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid . + /// + public void Add (HeaderId id, Encoding encoding, string value) + { + Add (new Header (encoding, id, value)); + } + + /// + /// Add a header with the specified field and value. + /// + /// + /// Adds a new header for the specified field and value pair. + /// + /// The name of the header field. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public void Add (string field, Encoding encoding, string value) + { + Add (new Header (encoding, field, value)); + } + + /// + /// Check if the contains a header with the specified field name. + /// + /// + /// Determines whether or not the header list contains the specified header. + /// + /// true if the requested header exists; + /// otherwise false. + /// The header identifier. + /// + /// is not a valid . + /// + public bool Contains (HeaderId id) + { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + return table.ContainsKey (id.ToHeaderName ()); + } + + /// + /// Check if the contains a header with the specified field name. + /// + /// + /// Determines whether or not the header list contains the specified header. + /// + /// true if the requested header exists; + /// otherwise false. + /// The name of the header field. + /// + /// is null. + /// + public bool Contains (string field) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + return table.ContainsKey (field); + } + + /// + /// Get the index of the requested header, if it exists. + /// + /// + /// Finds the first index of the specified header, if it exists. + /// + /// The index of the requested header; otherwise -1. + /// The header id. + /// + /// is not a valid . + /// + public int IndexOf (HeaderId id) + { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + for (int i = 0; i < headers.Count; i++) { + if (headers[i].Id == id) + return i; + } + + return -1; + } + + /// + /// Get the index of the requested header, if it exists. + /// + /// + /// Finds the first index of the specified header, if it exists. + /// + /// The index of the requested header; otherwise -1. + /// The name of the header field. + /// + /// is null. + /// + public int IndexOf (string field) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + for (int i = 0; i < headers.Count; i++) { + if (headers[i].Field.Equals (field, StringComparison.OrdinalIgnoreCase)) + return i; + } + + return -1; + } + + /// + /// Insert a header with the specified field and value at the given index. + /// + /// + /// Inserts the header at the specified index in the list. + /// + /// The index to insert the header. + /// The header identifier. + /// The header value. + /// + /// is null. + /// + /// + /// is not a valid . + /// -or- + /// is out of range. + /// + public void Insert (int index, HeaderId id, string value) + { + Insert (index, id, Encoding.UTF8, value); + } + + /// + /// Insert a header with the specified field and value at the given index. + /// + /// + /// Inserts the header at the specified index in the list. + /// + /// The index to insert the header. + /// The name of the header field. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + /// + /// is out of range. + /// + public void Insert (int index, string field, string value) + { + Insert (index, field, Encoding.UTF8, value); + } + + /// + /// Insert a header with the specified field and value at the given index. + /// + /// + /// Inserts the header at the specified index in the list. + /// + /// The index to insert the header. + /// The header identifier. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid . + /// -or- + /// is out of range. + /// + public void Insert (int index, HeaderId id, Encoding encoding, string value) + { + Insert (index, new Header (encoding, id, value)); + } + + /// + /// Insert a header with the specified field and value at the given index. + /// + /// + /// Inserts the header at the specified index in the list. + /// + /// The index to insert the header. + /// The name of the header field. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + /// + /// is out of range. + /// + public void Insert (int index, string field, Encoding encoding, string value) + { + Insert (index, new Header (encoding, field, value)); + } + + /// + /// Get the last index of the requested header, if it exists. + /// + /// + /// Finds the last index of the specified header, if it exists. + /// + /// The last index of the requested header; otherwise -1. + /// The header id. + /// + /// is not a valid . + /// + public int LastIndexOf (HeaderId id) + { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + for (int i = headers.Count - 1; i >= 0; i--) { + if (headers[i].Id == id) + return i; + } + + return -1; + } + + /// + /// Get the last index of the requested header, if it exists. + /// + /// + /// Finds the last index of the specified header, if it exists. + /// + /// The last index of the requested header; otherwise -1. + /// The name of the header field. + /// + /// is null. + /// + public int LastIndexOf (string field) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + for (int i = headers.Count - 1; i >= 0; i--) { + if (headers[i].Field.Equals (field, StringComparison.OrdinalIgnoreCase)) + return i; + } + + return -1; + } + + /// + /// Remove the first occurance of the specified header field. + /// + /// + /// Removes the first occurance of the specified header field, if any exist. + /// + /// true if the first occurance of the specified + /// header was removed; otherwise false. + /// The header identifier. + /// + /// is is not a valid . + /// + public bool Remove (HeaderId id) + { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + Header header; + if (!table.TryGetValue (id.ToHeaderName (), out header)) + return false; + + return Remove (header); + } + + /// + /// Remove the first occurance of the specified header field. + /// + /// + /// Removes the first occurance of the specified header field, if any exist. + /// + /// true if the first occurance of the specified + /// header was removed; otherwise false. + /// The name of the header field. + /// + /// is null. + /// + public bool Remove (string field) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + Header header; + if (!table.TryGetValue (field, out header)) + return false; + + return Remove (header); + } + + /// + /// Remove all of the headers matching the specified field name. + /// + /// + /// Removes all of the headers matching the specified field name. + /// + /// The header identifier. + /// + /// is not a valid . + /// + public void RemoveAll (HeaderId id) + { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + table.Remove (id.ToHeaderName ()); + + for (int i = headers.Count - 1; i >= 0; i--) { + if (headers[i].Id != id) + continue; + + var header = headers[i]; + headers.RemoveAt (i); + + OnChanged (header, HeaderListChangedAction.Removed); + } + } + + /// + /// Remove all of the headers matching the specified field name. + /// + /// + /// Removes all of the headers matching the specified field name. + /// + /// The name of the header field. + /// + /// is null. + /// + public void RemoveAll (string field) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + table.Remove (field); + + for (int i = headers.Count - 1; i >= 0; i--) { + if (!headers[i].Field.Equals (field, StringComparison.OrdinalIgnoreCase)) + continue; + + var header = headers[i]; + headers.RemoveAt (i); + + OnChanged (header, HeaderListChangedAction.Removed); + } + } + + /// + /// Replace all headers with identical field names with the single specified header. + /// + /// + /// Replaces all headers with identical field names with the single specified header. + /// If no headers with the specified field name exist, it is simply added. + /// + /// The header identifier. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a valid . + /// + public void Replace (HeaderId id, Encoding encoding, string value) + { + Replace (new Header (encoding, id, value)); + } + + /// + /// Replace all headers with identical field names with the single specified header. + /// + /// + /// Replaces all headers with identical field names with the single specified header. + /// If no headers with the specified field name exist, it is simply added. + /// + /// The header identifier. + /// The header value. + /// + /// is null. + /// + /// + /// is not a valid . + /// + public void Replace (HeaderId id, string value) + { + Replace (new Header (id, value)); + } + + /// + /// Replace all headers with identical field names with the single specified header. + /// + /// + /// Replaces all headers with identical field names with the single specified header. + /// If no headers with the specified field name exist, it is simply added. + /// + /// The name of the header field. + /// The character encoding to use for the value. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + public void Replace (string field, Encoding encoding, string value) + { + Replace (new Header (encoding, field, value)); + } + + /// + /// Replace all headers with identical field names with the single specified header. + /// + /// + /// Replaces all headers with identical field names with the single specified header. + /// If no headers with the specified field name exist, it is simply added. + /// + /// The name of the header field. + /// The header value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public void Replace (string field, string value) + { + Replace (new Header (field, value)); + } + + /// + /// Get or set the value of the first occurance of a header + /// with the specified field name. + /// + /// + /// Gets or sets the value of the first occurance of a header + /// with the specified field name. + /// + /// The value of the first occurrance of the specified header if it exists; otherwise null. + /// The header identifier. + /// + /// is null. + /// + public string this [HeaderId id] { + get { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + Header header; + if (table.TryGetValue (id.ToHeaderName (), out header)) + return header.Value; + + return null; + } + set { + if (id == HeaderId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Header header; + if (table.TryGetValue (id.ToHeaderName (), out header)) { + header.Value = value; + } else { + Add (id, value); + } + } + } + + /// + /// Get or set the value of the first occurance of a header + /// with the specified field name. + /// + /// + /// Gets or sets the value of the first occurance of a header + /// with the specified field name. + /// + /// The value of the first occurrance of the specified header if it exists; otherwise null. + /// The name of the header field. + /// + /// is null. + /// -or- + /// is null. + /// + public string this [string field] { + get { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + Header header; + if (table.TryGetValue (field, out header)) + return header.Value; + + return null; + } + set { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Header header; + if (table.TryGetValue (field, out header)) { + header.Value = value; + } else { + Add (field, value); + } + } + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes all of the headers to the output stream. + /// + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + foreach (var header in headers) { + filtered.Write (header.RawField, 0, header.RawField.Length, cancellationToken); + + if (!header.IsInvalid) { + var rawValue = header.GetRawValue (options); + + filtered.Write (Header.Colon, 0, Header.Colon.Length, cancellationToken); + filtered.Write (rawValue, 0, rawValue.Length, cancellationToken); + } + } + + filtered.Flush (cancellationToken); + } + + var cancellable = stream as ICancellableStream; + + if (cancellable != null) { + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Writes all of the headers to the output stream. + /// + /// A task that represents the asynchronous write operation. + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + foreach (var header in headers) { + await filtered.WriteAsync (header.RawField, 0, header.RawField.Length, cancellationToken).ConfigureAwait (false); + + if (!header.IsInvalid) { + var rawValue = header.GetRawValue (options); + + await filtered.WriteAsync (Header.Colon, 0, Header.Colon.Length, cancellationToken).ConfigureAwait (false); + await filtered.WriteAsync (rawValue, 0, rawValue.Length, cancellationToken).ConfigureAwait (false); + } + } + + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes all of the headers to the output stream. + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, stream, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Writes all of the headers to the output stream. + /// + /// A task that represents the asynchronous write operation. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, stream, cancellationToken); + } + + #region ICollection implementation + + /// + /// Get the number of headers in the list. + /// + /// + /// Gets the number of headers in the list. + /// + /// The number of headers. + public int Count { + get { return headers.Count; } + } + + /// + /// Get whether or not the header list is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add the specified header. + /// + /// + /// Adds the specified header to the end of the header list. + /// + /// The header to add. + /// + /// is null. + /// + public void Add (Header header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + if (!table.ContainsKey (header.Field)) + table.Add (header.Field, header); + + header.Changed += HeaderChanged; + headers.Add (header); + + OnChanged (header, HeaderListChangedAction.Added); + } + + /// + /// Clear the header list. + /// + /// + /// Removes all of the headers from the list. + /// + public void Clear () + { + foreach (var header in headers) + header.Changed -= HeaderChanged; + + headers.Clear (); + table.Clear (); + + OnChanged (null, HeaderListChangedAction.Cleared); + } + + /// + /// Check if the contains the specified header. + /// + /// + /// Determines whether or not the header list contains the specified header. + /// + /// true if the specified header is contained; + /// otherwise, false. + /// The header. + /// + /// is null. + /// + public bool Contains (Header header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + return headers.Contains (header); + } + + /// + /// Copy all of the headers in the to the specified array. + /// + /// + /// Copies all of the headers within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the headers to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (Header[] array, int arrayIndex) + { + headers.CopyTo (array, arrayIndex); + } + + /// + /// Remove the specified header. + /// + /// + /// Removes the specified header from the list if it exists. + /// + /// true if the specified header was removed; + /// otherwise false. + /// The header. + /// + /// is null. + /// + public bool Remove (Header header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + int index = headers.IndexOf (header); + + if (index == -1) + return false; + + header.Changed -= HeaderChanged; + + if (table[header.Field] == header) { + table.Remove (header.Field); + + // find the next matching header and add it to the lookup table + for (int i = index + 1; i < headers.Count; i++) { + if (headers[i].Field.Equals (header.Field, StringComparison.OrdinalIgnoreCase)) { + table.Add (headers[i].Field, headers[i]); + break; + } + } + } + + headers.RemoveAt (index); + + OnChanged (header, HeaderListChangedAction.Removed); + + return true; + } + + /// + /// Replace all headers with identical field names with the single specified header. + /// + /// + /// Replaces all headers with identical field names with the single specified header. + /// If no headers with the specified field name exist, it is simply added. + /// + /// The header. + /// + /// is null. + /// + public void Replace (Header header) + { + int i; + + if (header == null) + throw new ArgumentNullException (nameof (header)); + + Header first; + if (!table.TryGetValue (header.Field, out first)) { + Add (header); + return; + } + + for (i = headers.Count - 1; i >= 0; i--) { + if (headers[i] == first) + break; + + if (!headers[i].Field.Equals (header.Field, StringComparison.OrdinalIgnoreCase)) + continue; + + headers[i].Changed -= HeaderChanged; + headers.RemoveAt (i); + } + + header.Changed += HeaderChanged; + first.Changed -= HeaderChanged; + + table[header.Field] = header; + headers[i] = header; + + OnChanged (first, HeaderListChangedAction.Removed); + OnChanged (header, HeaderListChangedAction.Added); + } + + #endregion + + #region IList implementation + + /// + /// Get the index of the requested header, if it exists. + /// + /// + /// Finds the index of the specified header, if it exists. + /// + /// The index of the requested header; otherwise -1. + /// The header. + /// + /// is null. + /// + public int IndexOf (Header header) + { + if (header == null) + throw new ArgumentNullException (nameof (header)); + + return headers.IndexOf (header); + } + + /// + /// Insert the specified header at the given index. + /// + /// + /// Inserts the header at the specified index in the list. + /// + /// The index to insert the header. + /// The header. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, Header header) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (header == null) + throw new ArgumentNullException (nameof (header)); + + // update the lookup table + Header hdr; + if (table.TryGetValue (header.Field, out hdr)) { + int idx = headers.IndexOf (hdr); + + if (idx >= index) + table[header.Field] = header; + } else { + table.Add (header.Field, header); + } + + headers.Insert (index, header); + header.Changed += HeaderChanged; + + OnChanged (header, HeaderListChangedAction.Added); + } + + /// + /// Remove the header at the specified index. + /// + /// + /// Removes the header at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + var header = headers[index]; + + header.Changed -= HeaderChanged; + + if (table[header.Field] == header) { + table.Remove (header.Field); + + // find the next matching header and add it to the lookup table + for (int i = index + 1; i < headers.Count; i++) { + if (headers[i].Field.Equals (header.Field, StringComparison.OrdinalIgnoreCase)) { + table.Add (headers[i].Field, headers[i]); + break; + } + } + } + + headers.RemoveAt (index); + + OnChanged (header, HeaderListChangedAction.Removed); + } + + /// + /// Get or set the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The header at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public Header this [int index] { + get { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return headers[index]; + } + set { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + var header = headers[index]; + + if (header == value) + return; + + header.Changed -= HeaderChanged; + value.Changed += HeaderChanged; + + if (header.Field.Equals (value.Field, StringComparison.OrdinalIgnoreCase)) { + // replace the old header with the new one + if (table[header.Field] == header) + table[header.Field] = value; + } else { + // update the table for the header field being replaced + if (table[header.Field] == header) { + table.Remove (header.Field); + + // find the next matching header and add it to the lookup table + for (int i = index + 1; i < headers.Count; i++) { + if (headers[i].Field.Equals (header.Field, StringComparison.OrdinalIgnoreCase)) { + table.Add (headers[i].Field, headers[i]); + break; + } + } + } + + // update the table for the header being set + if (table.TryGetValue (value.Field, out header)) { + int idx = headers.IndexOf (header); + + if (idx > index) + table[header.Field] = value; + } else { + table.Add (value.Field, value); + } + } + + headers[index] = value; + + if (header.Field.Equals (value.Field, StringComparison.OrdinalIgnoreCase)) { + OnChanged (value, HeaderListChangedAction.Changed); + } else { + OnChanged (header, HeaderListChangedAction.Removed); + OnChanged (value, HeaderListChangedAction.Added); + } + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of headers. + /// + /// + /// Gets an enumerator for the list of headers. + /// + /// The enumerator. + public IEnumerator
GetEnumerator () + { + return headers.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of headers. + /// + /// + /// Gets an enumerator for the list of headers. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return headers.GetEnumerator (); + } + + #endregion + + internal event EventHandler Changed; + + void HeaderChanged (object sender, EventArgs args) + { + OnChanged ((Header) sender, HeaderListChangedAction.Changed); + } + + void OnChanged (Header header, HeaderListChangedAction action) + { + if (Changed != null) + Changed (this, new HeaderListChangedEventArgs (header, action)); + } + + internal bool TryGetHeader (string field, out Header header) + { + if (field == null) + throw new ArgumentNullException (nameof (field)); + + return table.TryGetValue (field, out header); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed list of headers. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static HeaderList Load (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity); + + return parser.ParseHeaders (cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed list of headers. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity); + + return parser.ParseHeadersAsync (cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed list of headers. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static HeaderList Load (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, stream, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed list of headers. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, stream, cancellationToken); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the specified . + /// + /// The parsed list of headers. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static HeaderList Load (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return Load (options, stream, cancellationToken); + } + + /// + /// Asynchronously load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the specified . + /// + /// The parsed list of headers. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static async Task LoadAsync (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return await LoadAsync (options, stream, cancellationToken).ConfigureAwait (false); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the default . + /// + /// The parsed list of headers. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static HeaderList Load (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, fileName, cancellationToken); + } + + /// + /// Asynchronously load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the default . + /// + /// The parsed list of headers. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, fileName, cancellationToken); + } + } +} diff --git a/src/MimeKit/HeaderListChangedEventArgs.cs b/src/MimeKit/HeaderListChangedEventArgs.cs new file mode 100644 index 0000000..7da76af --- /dev/null +++ b/src/MimeKit/HeaderListChangedEventArgs.cs @@ -0,0 +1,74 @@ +// +// HeaderChangedEventArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit { + /// + /// Header list changed action. + /// + /// + /// Specifies the way that a was changed. + /// + public enum HeaderListChangedAction { + /// + /// A header was added. + /// + Added, + + /// + /// A header was changed. + /// + Changed, + + /// + /// A header was removed. + /// + Removed, + + /// + /// The header list was cleared. + /// + Cleared + } + + class HeaderListChangedEventArgs : EventArgs + { + internal HeaderListChangedEventArgs (Header header, HeaderListChangedAction action) + { + Header = header; + Action = action; + } + + public HeaderListChangedAction Action { + get; private set; + } + + public Header Header { + get; private set; + } + } +} diff --git a/src/MimeKit/HeaderListCollection.cs b/src/MimeKit/HeaderListCollection.cs new file mode 100644 index 0000000..211e113 --- /dev/null +++ b/src/MimeKit/HeaderListCollection.cs @@ -0,0 +1,254 @@ +// +// HeaderListCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MimeKit { + /// + /// A collection of groups. + /// + /// + /// A collection of groups used with + /// . + /// + /// + public class HeaderListCollection : ICollection + { + readonly List groups; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public HeaderListCollection () + { + groups = new List (); + } + + /// + /// Gets the number of groups in the collection. + /// + /// + /// Gets the number of groups in the collection. + /// + /// The number of groups. + public int Count { + get { return groups.Count; } + } + + /// + /// Gets whether or not the header list collection is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Gets or sets the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The group of headers at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public HeaderList this [int index] { + get { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + return groups[index]; + } + set { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (groups[index] == value) + return; + + groups[index].Changed -= OnGroupChanged; + value.Changed += OnGroupChanged; + groups[index] = value; + } + } + + /// + /// Adds the group of headers to the collection. + /// + /// + /// Adds the group of headers to the collection. + /// + /// The group of headers. + /// + /// is null. + /// + public void Add (HeaderList group) + { + if (group == null) + throw new ArgumentNullException (nameof (@group)); + + group.Changed += OnGroupChanged; + groups.Add (group); + OnChanged (); + } + + /// + /// Clears the header list collection. + /// + /// + /// Removes all of the groups from the collection. + /// + public void Clear () + { + for (int i = 0; i < groups.Count; i++) + groups[i].Changed -= OnGroupChanged; + + groups.Clear (); + OnChanged (); + } + + /// + /// Checks if the collection contains the specified group of headers. + /// + /// + /// Determines whether or not the collection contains the specified group of headers. + /// + /// true if the specified group of headers is contained; + /// otherwise, false. + /// The group of headers. + /// + /// is null. + /// + public bool Contains (HeaderList group) + { + if (group == null) + throw new ArgumentNullException (nameof (@group)); + + return groups.Contains (group); + } + + /// + /// Copies all of the header groups in the to the specified array. + /// + /// + /// Copies all of the header groups within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the headers to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (HeaderList[] array, int arrayIndex) + { + groups.CopyTo (array, arrayIndex); + } + + /// + /// Removes the specified header group. + /// + /// + /// Removes the specified header group from the collection, if it exists. + /// + /// true if the specified header group was removed; + /// otherwise false. + /// The group of headers. + /// + /// is null. + /// + public bool Remove (HeaderList group) + { + if (group == null) + throw new ArgumentNullException (nameof (@group)); + + if (!groups.Remove (group)) + return false; + + group.Changed -= OnGroupChanged; + OnChanged (); + + return true; + } + + /// + /// Gets an enumerator for the groups of headers. + /// + /// + /// Gets an enumerator for the groups of headers. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return groups.GetEnumerator (); + } + + /// + /// Gets an enumerator for the groups of headers. + /// + /// + /// Gets an enumerator for the groups of headers. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + internal event EventHandler Changed; + + void OnChanged () + { + var handler = Changed; + + if (handler != null) + handler (this, EventArgs.Empty); + } + + void OnGroupChanged (object sender, HeaderListChangedEventArgs e) + { + OnChanged (); + } + } +} diff --git a/src/MimeKit/IMimeContent.cs b/src/MimeKit/IMimeContent.cs new file mode 100644 index 0000000..d14331c --- /dev/null +++ b/src/MimeKit/IMimeContent.cs @@ -0,0 +1,178 @@ +// +// IMimeContent.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MimeKit { + /// + /// An interface for content stream encapsulation as used by . + /// + /// + /// Implemented by . + /// + /// + /// + /// + public interface IMimeContent + { + /// + /// Get the content encoding. + /// + /// + /// If the is not encoded, this value will be + /// . Otherwise, it will be + /// set to the raw content encoding of the stream. + /// + /// The encoding. + ContentEncoding Encoding { get; } + + /// + /// Get the new-line format, if known. + /// + /// + /// This property is typically only set by the as it parses + /// the content of a and is only used as a hint when verifying + /// digital signatures. + /// + /// The new-line format, if known. + NewLineFormat? NewLineFormat { get; } + + /// + /// Get the content stream. + /// + /// + /// Gets the content stream. + /// + /// The stream. + Stream Stream { get; } + + /// + /// Open the decoded content stream. + /// + /// + /// Provides a means of reading the decoded content without having to first write it to another + /// stream using . + /// + /// The decoded content stream. + Stream Open (); + + /// + /// Decode the content stream into another stream. + /// + /// + /// If the content stream is encoded, this method will decode it into the output stream + /// using a suitable decoder based on the property, otherwise the + /// stream will be copied into the output stream as-is. + /// + /// + /// + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + void DecodeTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously decode the content stream into another stream. + /// + /// + /// If the content stream is encoded, this method will decode it into the output stream + /// using a suitable decoder based on the property, otherwise the + /// stream will be copied into the output stream as-is. + /// + /// + /// + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + Task DecodeToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Copy the content stream to the specified output stream. + /// + /// + /// This is equivalent to simply using + /// to copy the content stream to the output stream except that this method is cancellable. + /// If you want the decoded content, use + /// instead. + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + void WriteTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)); + + /// + /// Asynchronously copy the content stream to the specified output stream. + /// + /// + /// This is equivalent to simply using + /// to copy the content stream to the output stream except that this method is cancellable. + /// If you want the decoded content, use + /// instead. + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was cancelled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + Task WriteToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)); + } +} diff --git a/src/MimeKit/IO/BoundStream.cs b/src/MimeKit/IO/BoundStream.cs new file mode 100644 index 0000000..0bc2c79 --- /dev/null +++ b/src/MimeKit/IO/BoundStream.cs @@ -0,0 +1,718 @@ +// +// BoundStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MimeKit.IO { + /// + /// A bounded stream, confined to reading and writing data to a limited subset of the overall source stream. + /// + /// + /// Wraps an arbitrary stream, limiting I/O operations to a subset of the source stream. + /// If the is -1, then the end of the stream is unbound. + /// When a is set to parse a persistent stream, it will construct + /// s using bounded streams instead of loading the content into memory. + /// + public class BoundStream : Stream + { + long position; + bool disposed; + bool eos; + + /// + /// Initialize a new instance of the class. + /// + /// + /// If the is less than 0, then the end of the stream + /// is unbounded. + /// + /// The underlying stream. + /// The offset in the base stream that will mark the start of this substream. + /// The offset in the base stream that will mark the end of this substream. + /// true to leave the baseStream open after the + /// is disposed; otherwise, false. + /// + /// is null. + /// + /// + /// is less than zero. + /// -or- + /// is greater than or equal to zero + /// -and- is less than . + /// + public BoundStream (Stream baseStream, long startBoundary, long endBoundary, bool leaveOpen) + { + if (baseStream == null) + throw new ArgumentNullException (nameof (baseStream)); + + if (startBoundary < 0) + throw new ArgumentOutOfRangeException (nameof (startBoundary)); + + if (endBoundary >= 0 && endBoundary < startBoundary) + throw new ArgumentOutOfRangeException (nameof (endBoundary)); + + EndBoundary = endBoundary < 0 ? -1 : endBoundary; + StartBoundary = startBoundary; + BaseStream = baseStream; + LeaveOpen = leaveOpen; + position = 0; + eos = false; + } + + /// + /// Gets the underlying stream. + /// + /// + /// All I/O is performed on the base stream. + /// + /// The underlying stream. + public Stream BaseStream { + get; private set; + } + + /// + /// Gets the start boundary offset of the underlying stream. + /// + /// + /// The start boundary is the byte offset into the + /// that marks the beginning of the substream. + /// + /// The start boundary offset of the underlying stream. + public long StartBoundary { + get; private set; + } + + /// + /// Gets the end boundary offset of the underlying stream. + /// + /// + /// The end boundary is the byte offset into the + /// that marks the end of the substream. If the value is less than 0, + /// then the end of the stream is treated as unbound. + /// + /// The end boundary offset of the underlying stream. + public long EndBoundary { + get; private set; + } + + /// + /// Checks whether or not the underlying stream will remain open after + /// the is disposed. + /// + /// + /// Checks whether or not the underlying stream will remain open after + /// the is disposed. + /// + /// true if the underlying stream should remain open after the + /// is disposed; otherwise, false. + protected bool LeaveOpen { + get; private set; + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (BoundStream)); + } + + void CheckCanSeek () + { + if (!BaseStream.CanSeek) + throw new NotSupportedException ("The stream does not support seeking"); + } + + void CheckCanRead () + { + if (!BaseStream.CanRead) + throw new NotSupportedException ("The stream does not support reading"); + } + + void CheckCanWrite () + { + if (!BaseStream.CanWrite) + throw new NotSupportedException ("The stream does not support writing"); + } + + #region Stream implementation + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// The will only support reading if the + /// supports it. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return BaseStream.CanRead; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// The will only support writing if the + /// supports it. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return BaseStream.CanWrite; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// The will only support seeking if the + /// supports it. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return BaseStream.CanSeek; } + } + + /// + /// Checks whether or not I/O operations can timeout. + /// + /// + /// The will only support timing out if the + /// supports it. + /// + /// true if I/O operations can timeout; otherwise, false. + public override bool CanTimeout { + get { return BaseStream.CanTimeout; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// If the property is greater than or equal to 0, + /// then the length will be calculated by subtracting the + /// from the . If the end of the stream is unbound, then the + /// will be subtracted from the length of the + /// . + /// + /// The length of the stream in bytes. + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Length { + get { + CheckDisposed (); + + if (EndBoundary != -1) + return EndBoundary - StartBoundary; + + if (eos) + return position; + + return BaseStream.Length - StartBoundary; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// The is relative to the . + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return position; } + set { Seek (value, SeekOrigin.Begin); } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// + /// Gets or sets the 's read timeout. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + public override int ReadTimeout { + get { return BaseStream.ReadTimeout; } + set { BaseStream.ReadTimeout = value; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// + /// Gets or sets the 's write timeout. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + public override int WriteTimeout { + get { return BaseStream.WriteTimeout; } + set { BaseStream.WriteTimeout = value; } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads data from the , not allowing it to + /// read beyond the . + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + // if we are at the end of the stream, we cannot read anymore data + if (EndBoundary != -1 && StartBoundary + position >= EndBoundary) { + eos = true; + return 0; + } + + // make sure that the source stream is in the expected position + if (BaseStream.Position != StartBoundary + position) + BaseStream.Seek (StartBoundary + position, SeekOrigin.Begin); + + int n = EndBoundary != -1 ? (int) Math.Min (EndBoundary - (StartBoundary + position), count) : count; + int nread = BaseStream.Read (buffer, offset, n); + + if (nread > 0) + position += nread; + else if (nread == 0) + eos = true; + + return nread; + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads data from the , not allowing it to + /// read beyond the . + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + // if we are at the end of the stream, we cannot read anymore data + if (EndBoundary != -1 && StartBoundary + position >= EndBoundary) { + eos = true; + return 0; + } + + // make sure that the source stream is in the expected position + if (BaseStream.Position != StartBoundary + position) + BaseStream.Seek (StartBoundary + position, SeekOrigin.Begin); + + int n = EndBoundary != -1 ? (int) Math.Min (EndBoundary - (StartBoundary + position), count) : count; + int nread = await BaseStream.ReadAsync (buffer, offset, n, cancellationToken).ConfigureAwait (false); + + if (nread > 0) + position += nread; + else if (nread == 0) + eos = true; + + return nread; + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes data to the , not allowing it to + /// write beyond the . + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + // if we are at the end of the stream, we cannot write anymore data + if (EndBoundary != -1 && StartBoundary + position + count > EndBoundary) { + eos = StartBoundary + position >= EndBoundary; + throw new IOException (); + } + + // make sure that the source stream is in the expected position + if (BaseStream.Position != StartBoundary + position) + BaseStream.Seek (StartBoundary + position, SeekOrigin.Begin); + + BaseStream.Write (buffer, offset, count); + position += count; + + if (EndBoundary != -1 && StartBoundary + position >= EndBoundary) + eos = true; + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes data to the , not allowing it to + /// write beyond the . + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + // if we are at the end of the stream, we cannot write anymore data + if (EndBoundary != -1 && StartBoundary + position + count > EndBoundary) { + eos = StartBoundary + position >= EndBoundary; + throw new IOException (); + } + + // make sure that the source stream is in the expected position + if (BaseStream.Position != StartBoundary + position) + BaseStream.Seek (StartBoundary + position, SeekOrigin.Begin); + + await BaseStream.WriteAsync (buffer, offset, count, cancellationToken).ConfigureAwait (false); + position += count; + + if (EndBoundary != -1 && StartBoundary + position >= EndBoundary) + eos = true; + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Seeks within the confines of the and the . + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support seeking. + /// + /// + /// An I/O error occurred. + /// + public override long Seek (long offset, SeekOrigin origin) + { + CheckDisposed (); + CheckCanSeek (); + + long real; + + switch (origin) { + case SeekOrigin.Begin: + real = StartBoundary + offset; + break; + case SeekOrigin.Current: + real = StartBoundary + position + offset; + break; + case SeekOrigin.End: + if (offset >= 0 || (EndBoundary == -1 && !eos)) { + // We don't know if the underlying stream can seek past the end or not... + if ((real = BaseStream.Seek (offset, origin)) == -1) + return -1; + } else if (EndBoundary == -1) { + // seeking backwards from eos (which happens to be our current position) + real = StartBoundary + position + offset; + } else { + // seeking backwards from a known position + real = EndBoundary + offset; + } + + break; + default: + throw new ArgumentOutOfRangeException (nameof (origin), "Invalid SeekOrigin specified"); + } + + // sanity check the resultant offset + if (real < StartBoundary) + throw new IOException ("Cannot seek to a position before the beginning of the stream"); + + // short-cut if we are seeking to our current position + if (real == StartBoundary + position) + return position; + + if (EndBoundary != -1 && real > EndBoundary) + throw new IOException ("Cannot seek beyond the end of the stream"); + + if ((real = BaseStream.Seek (real, SeekOrigin.Begin)) == -1) + return -1; + + // reset eos if appropriate + if ((EndBoundary != -1 && real < EndBoundary) || (eos && real < StartBoundary + position)) + eos = false; + + position = real - StartBoundary; + + return position; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Flushes the . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + CheckDisposed (); + CheckCanWrite (); + + BaseStream.Flush (); + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Flushes the . + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + return BaseStream.FlushAsync (cancellationToken); + } + + /// + /// Sets the length of the stream. + /// + /// + /// Updates the to be plus + /// the specified new length. If the needs to be grown + /// to allow this, then the length of the will also be + /// updated. + /// + /// The desired length of the stream in bytes. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support setting the length. + /// + /// + /// An I/O error occurred. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + if (EndBoundary == -1 || StartBoundary + value > EndBoundary) { + long end = BaseStream.Length; + + if (StartBoundary + value > end) + BaseStream.SetLength (StartBoundary + value); + + EndBoundary = StartBoundary + value; + } else { + EndBoundary = StartBoundary + value; + } + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// If the property is false, then + /// the is also disposed. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !LeaveOpen) + BaseStream.Dispose (); + + base.Dispose (disposing); + disposed = true; + } + + #endregion + } +} diff --git a/src/MimeKit/IO/ChainedStream.cs b/src/MimeKit/IO/ChainedStream.cs new file mode 100644 index 0000000..7561e4c --- /dev/null +++ b/src/MimeKit/IO/ChainedStream.cs @@ -0,0 +1,700 @@ +// +// ChainedStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace MimeKit.IO { + /// + /// A chained stream. + /// + /// + /// Chains multiple streams together such that reading or writing beyond the end + /// of one stream spills over into the next stream in the chain. The idea is to + /// make it appear is if the chain of streams is all one continuous stream. + /// + public class ChainedStream : Stream + { + readonly List streams; + readonly List leaveOpen; + long position; + bool disposed; + int current; + bool eos; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public ChainedStream () + { + leaveOpen = new List (); + streams = new List (); + } + + /// + /// Add the specified stream to the chained stream. + /// + /// + /// Adds the stream to the end of the chain. + /// + /// The stream. + /// true if the + /// should remain open after the is disposed; + /// otherwise, false. + /// + /// is null. + /// + public void Add (Stream stream, bool leaveOpen = false) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + this.leaveOpen.Add (leaveOpen); + streams.Add (stream); + eos = false; + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (ChainedStream)); + } + + void CheckCanSeek () + { + if (!CanSeek) + throw new NotSupportedException ("The stream does not support seeking"); + } + + void CheckCanRead () + { + if (!CanRead) + throw new NotSupportedException ("The stream does not support reading"); + } + + void CheckCanWrite () + { + if (!CanWrite) + throw new NotSupportedException ("The stream does not support writing"); + } + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// The only supports reading if all of its + /// streams support it. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { + foreach (var stream in streams) { + if (!stream.CanRead) + return false; + } + + return streams.Count > 0; + } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// The only supports writing if all of its + /// streams support it. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { + foreach (var stream in streams) { + if (!stream.CanWrite) + return false; + } + + return streams.Count > 0; + } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// The only supports seeking if all of its + /// streams support it. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { + foreach (var stream in streams) { + if (!stream.CanSeek) + return false; + } + + return streams.Count > 0; + } + } + + /// + /// Checks whether or not I/O operations can timeout. + /// + /// + /// The only supports timeouts if all of its + /// streams support them. + /// + /// true if I/O operations can timeout; otherwise, false. + public override bool CanTimeout { + get { return false; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// The length of a is the combined lenths of all + /// of its chained streams. + /// + /// The length of the stream in bytes. + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Length { + get { + long length = 0; + + CheckDisposed (); + + foreach (var stream in streams) + length += stream.Length; + + return length; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// It is always possible to get the position of a , + /// but setting the position is only possible if all of its streams are seekable. + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return position; } + set { Seek (value, SeekOrigin.Begin); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads up to the requested number of bytes if reading is supported. If the + /// current child stream does not have enough remaining data to complete the + /// read, the read will progress into the next stream in the chain in order + /// to complete the read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + if (count == 0 || eos) + return 0; + + int n, nread = 0; + + while (current < streams.Count) { + while (nread < count && (n = streams[current].Read (buffer, offset + nread, count - nread)) > 0) + nread += n; + + if (nread == count) + break; + + current++; + } + + if (nread > 0) + position += nread; + else + eos = true; + + return nread; + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads up to the requested number of bytes if reading is supported. If the + /// current child stream does not have enough remaining data to complete the + /// read, the read will progress into the next stream in the chain in order + /// to complete the read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + if (count == 0 || eos) + return 0; + + int n, nread = 0; + + while (current < streams.Count) { + while (nread < count) { + if ((n = await streams[current].ReadAsync (buffer, offset + nread, count - nread, cancellationToken).ConfigureAwait (false)) <= 0) + break; + + nread += n; + } + + if (nread == count) + break; + + current++; + } + + if (nread > 0) + position += nread; + else + eos = true; + + return nread; + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes the requested number of bytes if writing is supported. If the + /// current child stream does not have enough remaining space to fit the + /// complete buffer, the data will spill over into the next stream in the + /// chain in order to complete the write. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + if (current >= streams.Count) + current = streams.Count - 1; + + int nwritten = 0; + + while (current < streams.Count && nwritten < count) { + int n = count - nwritten; + + if (current + 1 < streams.Count) { + long left = streams[current].Length - streams[current].Position; + + if (left < n) + n = (int) left; + } + + streams[current].Write (buffer, offset + nwritten, n); + position += n; + nwritten += n; + + if (nwritten < count) { + streams[current].Flush (); + current++; + } + } + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes the requested number of bytes if writing is supported. If the + /// current child stream does not have enough remaining space to fit the + /// complete buffer, the data will spill over into the next stream in the + /// chain in order to complete the write. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + if (current >= streams.Count) + current = streams.Count - 1; + + int nwritten = 0; + + while (current < streams.Count && nwritten < count) { + int n = count - nwritten; + + if (current + 1 < streams.Count) { + long left = streams[current].Length - streams[current].Position; + + if (left < n) + n = (int) left; + } + + await streams[current].WriteAsync (buffer, offset + nwritten, n, cancellationToken).ConfigureAwait (false); + position += n; + nwritten += n; + + if (nwritten < count) { + await streams[current].FlushAsync (cancellationToken).ConfigureAwait (false); + current++; + } + } + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Seeks to the specified position within the stream if all child streams + /// support seeking. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support seeking. + /// + /// + /// An I/O error occurred. + /// + public override long Seek (long offset, SeekOrigin origin) + { + CheckDisposed (); + CheckCanSeek (); + + long length = -1; + long real; + + switch (origin) { + case SeekOrigin.Begin: + real = offset; + break; + case SeekOrigin.Current: + real = position + offset; + break; + case SeekOrigin.End: + length = Length; + real = length + offset; + break; + default: + throw new ArgumentOutOfRangeException (nameof (origin), "Invalid SeekOrigin specified"); + } + + // sanity check the resultant offset + if (real < 0) + throw new IOException ("Cannot seek to a position before the beginning of the stream"); + + // short-cut if we are seeking to our current position + if (real == position) + return position; + + if (real > (length < 0 ? Length : length)) + throw new IOException ("Cannot seek beyond the end of the stream"); + + if (real > position) { + while (current < streams.Count && position < real) { + long left = streams[current].Length - streams[current].Position; + long n = Math.Min (left, real - position); + + streams[current].Seek (n, SeekOrigin.Current); + position += n; + + if (position < real) + current++; + } + + eos = current >= streams.Count; + } else { + int max = Math.Min (streams.Count - 1, current); + int cur = 0; + + position = 0; + while (cur <= max) { + length = streams[cur].Length; + + if (real < position + length) { + // this is the stream which encompasses our seek offset + streams[cur].Seek (real - position, SeekOrigin.Begin); + position = real; + break; + } + + position += length; + cur++; + } + + current = cur++; + + // reset any streams between our new current stream and our old current stream + while (cur <= max) + streams[cur++].Seek (0, SeekOrigin.Begin); + + eos = false; + } + + return position; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// If all of the child streams support writing, then the current child stream + /// will be flushed. + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + CheckDisposed (); + CheckCanWrite (); + + if (current < streams.Count) + streams[current].Flush (); + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// If all of the child streams support writing, then the current child stream + /// will be flushed. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + if (current < streams.Count) + await streams[current].FlushAsync (cancellationToken).ConfigureAwait (false); + } + + /// + /// Sets the length of the stream. + /// + /// + /// Setting the length of a is not supported. + /// + /// The desired length of the stream in bytes. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + throw new NotSupportedException ("Cannot set a length on the stream"); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + for (int i = 0; i < streams.Count; i++) { + if (!leaveOpen[i]) + streams[i].Dispose (); + } + } + + base.Dispose (disposing); + disposed = true; + } + } +} diff --git a/src/MimeKit/IO/FilteredStream.cs b/src/MimeKit/IO/FilteredStream.cs new file mode 100644 index 0000000..604bc8f --- /dev/null +++ b/src/MimeKit/IO/FilteredStream.cs @@ -0,0 +1,812 @@ +// +// FilteredStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit.IO.Filters; + +namespace MimeKit.IO { + /// + /// A stream which filters data as it is read or written. + /// + /// + /// Passes data through each as the data is read or written. + /// + public class FilteredStream : Stream, ICancellableStream + { + const int ReadBufferSize = 4096; + + enum IOOperation : byte { + Read, + Write + } + + List filters = new List (); + IOOperation lastOp = IOOperation.Write; + int filteredLength; + int filteredIndex; + byte[] filtered; + byte[] readbuf; + bool disposed; + bool flushed; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a filtered stream using the specified source stream. + /// + /// The underlying stream to filter. + /// + /// is null. + /// + public FilteredStream (Stream source) + { + if (source == null) + throw new ArgumentNullException (nameof (source)); + + Source = source; + } + + /// + /// Gets the underlying source stream. + /// + /// + /// In general, it is not a good idea to manipulate the underlying + /// source stream because most s store + /// important state about previous bytes read from or written to + /// the source stream. + /// + /// The underlying source stream. + public Stream Source { + get; private set; + } + + /// + /// Adds the specified filter. + /// + /// + /// Adds the to the end of the list of filters + /// that data will pass through as data is read from or written to the + /// underlying source stream. + /// + /// The filter. + /// + /// is null. + /// + public void Add (IMimeFilter filter) + { + if (filter == null) + throw new ArgumentNullException (nameof (filter)); + + filters.Add (filter); + } + + /// + /// Checks if the filtered stream contains the specified filter. + /// + /// + /// Determines whether or not the filtered stream contains the specified filter. + /// + /// true if the specified filter exists; + /// otherwise false. + /// The filter. + /// + /// is null. + /// + public bool Contains (IMimeFilter filter) + { + if (filter == null) + throw new ArgumentNullException (nameof (filter)); + + return filters.Contains (filter); + } + + /// + /// Remove the specified filter. + /// + /// + /// Removes the specified filter from the list if it exists. + /// + /// true if the filter was removed; otherwise false. + /// The filter. + /// + /// is null. + /// + public bool Remove (IMimeFilter filter) + { + if (filter == null) + throw new ArgumentNullException (nameof (filter)); + + return filters.Remove (filter); + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (FilteredStream)); + } + + void CheckCanRead () + { + if (!Source.CanRead) + throw new NotSupportedException ("The stream does not support reading"); + } + + void CheckCanWrite () + { + if (!Source.CanWrite) + throw new NotSupportedException ("The stream does not support writing"); + } + + #region Stream implementation + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// The will only support reading if the + /// supports it. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return Source.CanRead; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// The will only support writing if the + /// supports it. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return Source.CanWrite; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// Seeking is not supported by the . + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return false; } + } + + /// + /// Checks whether or not I/O operations can timeout. + /// + /// + /// The will only support timing out if the + /// supports it. + /// + /// true if I/O operations can timeout; otherwise, false. + public override bool CanTimeout { + get { return Source.CanTimeout; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// Getting the length of a is not supported. + /// + /// The length of the stream in bytes. + /// + /// The stream does not support seeking. + /// + public override long Length { + get { throw new NotSupportedException ("Cannot get the length of the stream"); } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// Getting and setting the position of a is not supported. + /// + /// The position of the stream. + /// + /// The stream does not support seeking. + /// + public override long Position { + get { throw new NotSupportedException ("The stream does not support seeking"); } + set { throw new NotSupportedException ("The stream does not support seeking"); } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to read before timing out. + /// + /// + /// Gets or sets the read timeout on the stream. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to read before timing out. + public override int ReadTimeout + { + get { return Source.ReadTimeout; } + set { Source.ReadTimeout = value; } + } + + /// + /// Gets or sets a value, in miliseconds, that determines how long the stream will attempt to write before timing out. + /// + /// + /// Gets or sets the write timeout on the stream. + /// + /// A value, in miliseconds, that determines how long the stream will attempt to write before timing out. + public override int WriteTimeout + { + get { return Source.WriteTimeout; } + set { Source.WriteTimeout = value; } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads up to the requested number of bytes, passing the data read from the stream + /// through each of the filters before finally copying the result into the provided buffer. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + lastOp = IOOperation.Read; + if (readbuf == null) + readbuf = new byte[ReadBufferSize]; + + int nread; + + if (filteredLength == 0) { + var cancellable = Source as ICancellableStream; + + if (cancellable != null) { + if ((nread = cancellable.Read (readbuf, 0, ReadBufferSize, cancellationToken)) <= 0) + return nread; + } else { + cancellationToken.ThrowIfCancellationRequested (); + if ((nread = Source.Read (readbuf, 0, ReadBufferSize)) <= 0) + return nread; + } + + // filter the data we've just read... + filteredLength = nread; + filteredIndex = 0; + filtered = readbuf; + + foreach (var filter in filters) + filtered = filter.Filter (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + } + + // copy our filtered data into our caller's buffer + nread = Math.Min (filteredLength, count); + + if (nread > 0) { + Buffer.BlockCopy (filtered, filteredIndex, buffer, offset, nread); + filteredLength -= nread; + filteredIndex += nread; + } + + return nread; + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads up to the requested number of bytes, passing the data read from the stream + /// through each of the filters before finally copying the result into the provided buffer. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + return Read (buffer, offset, count, CancellationToken.None); + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads up to the requested number of bytes, passing the data read from the stream + /// through each of the filters before finally copying the result into the provided buffer. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanRead (); + + ValidateArguments (buffer, offset, count); + + lastOp = IOOperation.Read; + if (readbuf == null) + readbuf = new byte[ReadBufferSize]; + + int nread; + + if (filteredLength == 0) { + if ((nread = await Source.ReadAsync (readbuf, 0, ReadBufferSize, cancellationToken).ConfigureAwait (false)) <= 0) + return nread; + + // filter the data we've just read... + filteredLength = nread; + filteredIndex = 0; + filtered = readbuf; + + foreach (var filter in filters) + filtered = filter.Filter (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + } + + // copy our filtered data into our caller's buffer + nread = Math.Min (filteredLength, count); + + if (nread > 0) { + Buffer.BlockCopy (filtered, filteredIndex, buffer, offset, nread); + filteredLength -= nread; + filteredIndex += nread; + } + + return nread; + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Filters the provided buffer through each of the filters before finally writing + /// the result to the underlying stream. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + lastOp = IOOperation.Write; + flushed = false; + + filteredIndex = offset; + filteredLength = count; + filtered = buffer; + + foreach (var filter in filters) + filtered = filter.Filter (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + + if (filteredLength == 0) + return; + + var cancellable = Source as ICancellableStream; + + if (cancellable != null) { + cancellable.Write (filtered, filteredIndex, filteredLength, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + Source.Write (filtered, filteredIndex, filteredLength); + } + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Filters the provided buffer through each of the filters before finally writing + /// the result to the underlying stream. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + Write (buffer, offset, count, CancellationToken.None); + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Filters the provided buffer through each of the filters before finally writing + /// the result to the underlying stream. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + ValidateArguments (buffer, offset, count); + + lastOp = IOOperation.Write; + flushed = false; + + filteredIndex = offset; + filteredLength = count; + filtered = buffer; + + foreach (var filter in filters) + filtered = filter.Filter (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + + return Source.WriteAsync (filtered, filteredIndex, filteredLength, cancellationToken); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Seeking is not supported by the . + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// The stream does not support seeking. + /// + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException ("The stream does not support seeking"); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Flushes the state of all filters, writing any output to the underlying + /// stream and then calling on the . + /// + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void Flush (CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + if (lastOp == IOOperation.Read) + return; + + if (!flushed) { + filtered = new byte[0]; + filteredIndex = 0; + filteredLength = 0; + + foreach (var filter in filters) + filtered = filter.Flush (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + + flushed = true; + } + + var cancellable = Source as ICancellableStream; + + if (filteredLength > 0) { + if (cancellable != null) { + cancellable.Write (filtered, filteredIndex, filteredLength, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + Source.Write (filtered, filteredIndex, filteredLength); + } + + filteredIndex = 0; + filteredLength = 0; + } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Flushes the state of all filters, writing any output to the underlying + /// stream and then calling on the . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Flush () + { + Flush (CancellationToken.None); + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Flushes the state of all filters, writing any output to the underlying + /// stream and then calling on the . + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + CheckCanWrite (); + + if (lastOp == IOOperation.Read) + return; + + if (!flushed) { + filtered = new byte[0]; + filteredIndex = 0; + filteredLength = 0; + + foreach (var filter in filters) + filtered = filter.Flush (filtered, filteredIndex, filteredLength, out filteredIndex, out filteredLength); + + flushed = true; + } + + if (filteredLength > 0) { + await Source.WriteAsync (filtered, filteredIndex, filteredLength, cancellationToken).ConfigureAwait (false); + + filteredIndex = 0; + filteredLength = 0; + } + } + + /// + /// Sets the length of the stream. + /// + /// + /// Setting the length of a is not supported. + /// + /// The desired length of the stream in bytes. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support setting the length. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + throw new NotSupportedException ("Cannot set a length on the stream"); + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing) { + if (filters != null) { + filters.Clear (); + filters = null; + } + + readbuf = null; + } + + base.Dispose (disposing); + disposed = true; + } + + #endregion + } +} diff --git a/src/MimeKit/IO/Filters/ArmoredFromFilter.cs b/src/MimeKit/IO/Filters/ArmoredFromFilter.cs new file mode 100644 index 0000000..48237a2 --- /dev/null +++ b/src/MimeKit/IO/Filters/ArmoredFromFilter.cs @@ -0,0 +1,167 @@ +// +// ArmoredFromFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; + +namespace MimeKit.IO.Filters { + /// + /// A filter that armors lines beginning with "From " by encoding the 'F' with the + /// Quoted-Printable encoding. + /// + /// + /// From-armoring is a workaround to prevent receiving clients (or servers) + /// that uses the mbox file format for local storage from munging the line + /// by prepending a ">", as is typical with the mbox format. + /// This armoring technique ensures that the receiving client will still + /// be able to verify S/MIME signatures. + /// + public class ArmoredFromFilter : MimeFilterBase + { + const string From = "From "; + bool midline; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public ArmoredFromFilter () + { + } + + static bool StartsWithFrom (byte[] input, int startIndex, int endIndex) + { + for (int i = 0, index = startIndex; i < From.Length && index < endIndex; i++, index++) { + if (input[index] != (byte) From[i]) + return false; + } + + return true; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + var fromOffsets = new List (); + int endIndex = startIndex + length; + int index = startIndex; + int left; + + while (index < endIndex) { + byte c = 0; + + if (midline) { + while (index < endIndex) { + c = input[index++]; + if (c == (byte) '\n') + break; + } + } + + if (c == (byte) '\n' || !midline) { + if ((left = endIndex - index) > 0) { + midline = true; + + if (left < 5) { + if (StartsWithFrom (input, index, endIndex)) { + SaveRemainingInput (input, index, left); + endIndex = index; + midline = false; + break; + } + } else { + if (StartsWithFrom (input, index, endIndex)) { + fromOffsets.Add (index); + index += 5; + } + } + } else { + midline = false; + } + } + } + + if (fromOffsets.Count > 0) { + int need = (endIndex - startIndex) + fromOffsets.Count * 2; + + EnsureOutputSize (need, false); + outputLength = 0; + outputIndex = 0; + + index = startIndex; + foreach (var offset in fromOffsets) { + if (index < offset) { + Buffer.BlockCopy (input, index, OutputBuffer, outputLength, offset - index); + outputLength += offset - index; + index = offset; + } + + // encode the F using quoted-printable + OutputBuffer[outputLength++] = (byte) '='; + OutputBuffer[outputLength++] = (byte) '4'; + OutputBuffer[outputLength++] = (byte) '6'; + index++; + } + + Buffer.BlockCopy (input, index, OutputBuffer, outputLength, endIndex - index); + outputLength += endIndex - index; + + return OutputBuffer; + } + + outputLength = endIndex - startIndex; + outputIndex = 0; + return input; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + midline = false; + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/BestEncodingFilter.cs b/src/MimeKit/IO/Filters/BestEncodingFilter.cs new file mode 100644 index 0000000..557d851 --- /dev/null +++ b/src/MimeKit/IO/Filters/BestEncodingFilter.cs @@ -0,0 +1,273 @@ +// +// BestEncodingFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.IO.Filters { + /// + /// A filter that can be used to determine the most efficient Content-Transfer-Encoding. + /// + /// + /// Keeps track of the content that gets passed through the filter in order to + /// determine the most efficient to use. + /// + public class BestEncodingFilter : IMimeFilter + { + readonly byte[] marker = new byte[6]; + int maxline, linelen; + int count0, count8; + int markerLength; + bool hasMarker; + int total; + byte pc; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public BestEncodingFilter () + { + } + + /// + /// Gets the best encoding given the specified constraints. + /// + /// + /// Gets the best encoding given the specified constraints. + /// + /// The best encoding. + /// The encoding constraint. + /// The maximum allowable line length (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public ContentEncoding GetBestEncoding (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + switch (constraint) { + case EncodingConstraint.SevenBit: + if (count0 > 0) + return ContentEncoding.Base64; + + if (count8 > 0) { + if (count8 >= (int) (total * (17.0 / 100.0))) + return ContentEncoding.Base64; + + return ContentEncoding.QuotedPrintable; + } + + if (hasMarker || maxline > maxLineLength) + return ContentEncoding.QuotedPrintable; + + break; + case EncodingConstraint.EightBit: + if (count0 > 0) + return ContentEncoding.Base64; + + if (hasMarker || maxline > maxLineLength) + return ContentEncoding.QuotedPrintable; + + if (count8 > 0) + return ContentEncoding.EightBit; + + break; + case EncodingConstraint.None: + if (hasMarker || maxline > maxLineLength) { + if (count8 > (int) (total * (17.0 / 100.0))) + return ContentEncoding.Base64; + + return ContentEncoding.QuotedPrintable; + } + + if (count0 > 0) + return ContentEncoding.Binary; + + if (count8 > 0) + return ContentEncoding.EightBit; + + break; + default: + throw new ArgumentOutOfRangeException (nameof (constraint)); + } + + return ContentEncoding.SevenBit; + } + + #region IMimeFilter implementation + + static unsafe bool IsMboxMarker (byte[] marker) + { + const uint FromMask = 0xFFFFFFFF; + const uint From = 0x6D6F7246; + + fixed (byte* buf = marker) { + uint* word = (uint*) buf; + + if ((*word & FromMask) != From) + return false; + + return *(buf + 4) == (byte) ' '; + } + } + + unsafe void Scan (byte* inptr, byte* inend) + { + while (inptr < inend) { + byte c = 0; + + while (inptr < inend && (c = *inptr++) != (byte) '\n') { + if (c == 0) + count0++; + else if ((c & 0x80) != 0) + count8++; + + if (!hasMarker && markerLength < 5) + marker[markerLength++] = c; + + linelen++; + pc = c; + } + + if (c == (byte) '\n') { + if (pc == (byte) '\r') + linelen--; + + maxline = Math.Max (maxline, linelen); + linelen = 0; + + // check our from-save buffer for "From " + if (!hasMarker && markerLength == 5 && IsMboxMarker (marker)) + hasMarker = true; + + markerLength = 0; + } + } + } + + static void ValidateArguments (byte[] input, int startIndex, int length) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + } + + /// + /// Filters the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + ValidateArguments (input, startIndex, length); + + unsafe { + fixed (byte* inptr = input) { + Scan (inptr + startIndex, inptr + startIndex + length); + } + } + + maxline = Math.Max (maxline, linelen); + total += length; + + outputIndex = startIndex; + outputLength = length; + + return input; + } + + /// + /// Filters the specified input, flushing all internally buffered data to the output. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public byte[] Flush (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + return Filter (input, startIndex, length, out outputIndex, out outputLength); + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public void Reset () + { + hasMarker = false; + markerLength = 0; + linelen = 0; + maxline = 0; + count0 = 0; + count8 = 0; + total = 0; + pc = 0; + } + + #endregion + } +} + diff --git a/src/MimeKit/IO/Filters/CharsetFilter.cs b/src/MimeKit/IO/Filters/CharsetFilter.cs new file mode 100644 index 0000000..d673fe3 --- /dev/null +++ b/src/MimeKit/IO/Filters/CharsetFilter.cs @@ -0,0 +1,229 @@ +// +// CharsetFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit.Utils; + +namespace MimeKit.IO.Filters { + /// + /// A charset filter for incrementally converting text streams from + /// one charset encoding to another. + /// + /// + /// Incrementally converts text from one charset encoding to another. + /// + public class CharsetFilter : MimeFilterBase + { + readonly char[] chars = new char[1024]; + readonly Decoder decoder; + readonly Encoder encoder; + + static Encoding GetEncoding (string paramName, string encodingName) + { + if (encodingName == null) + throw new ArgumentNullException (paramName); + + return CharsetUtils.GetEncoding (encodingName); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new to convert text from the specified + /// source encoding into the target charset encoding. + /// + /// Source encoding name. + /// Target encoding name. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The is not supported by the system. + /// -or- + /// The is not supported by the system. + /// + public CharsetFilter (string sourceEncodingName, string targetEncodingName) + : this (GetEncoding ("sourceEncodingName", sourceEncodingName), + GetEncoding ("targetEncodingName", targetEncodingName)) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new to convert text from the specified + /// source encoding into the target charset encoding. + /// + /// Source code page. + /// Target code page. + /// + /// is less than zero or greater than 65535. + /// -or- + /// is less than zero or greater than 65535. + /// + /// + /// The is not supported by the system. + /// -or- + /// The is not supported by the system. + /// + public CharsetFilter (int sourceCodePage, int targetCodePage) + : this (Encoding.GetEncoding (sourceCodePage), Encoding.GetEncoding (targetCodePage)) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new to convert text from the specified + /// source encoding into the target charset encoding. + /// + /// Source encoding. + /// Target encoding. + /// + /// is null. + /// -or- + /// is null. + /// + public CharsetFilter (Encoding sourceEncoding, Encoding targetEncoding) + { + if (sourceEncoding == null) + throw new ArgumentNullException (nameof (sourceEncoding)); + + if (targetEncoding == null) + throw new ArgumentNullException (nameof (targetEncoding)); + + SourceEncoding = sourceEncoding; + TargetEncoding = targetEncoding; + + decoder = (Decoder) SourceEncoding.GetDecoder (); + encoder = (Encoder) TargetEncoding.GetEncoder (); + } + + /// + /// Gets the source encoding. + /// + /// + /// Gets the source encoding. + /// + /// The source encoding. + public Encoding SourceEncoding { + get; private set; + } + + /// + /// Gets the target encoding. + /// + /// + /// Gets the target encoding. + /// + /// The target encoding. + public Encoding TargetEncoding { + get; private set; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + int inputIndex = startIndex; + int inputLeft = length; + bool decoded = false; + bool encoded = false; + int nwritten, nread; + int outputOffset = 0; + int outputLeft; + int charIndex; + int charsLeft; + + do { + charsLeft = chars.Length; + charIndex = 0; + + if (!decoded && inputLeft > 0) { + decoder.Convert (input, inputIndex, inputLeft, chars, charIndex, charsLeft, flush, out nread, out nwritten, out decoded); + if (nwritten > 0) + encoded = false; + charIndex += nwritten; + inputIndex += nread; + inputLeft -= nread; + } else { + decoded = true; + } + + charsLeft = charIndex; + charIndex = 0; + + // encode *all* input chars into the output buffer + while (!encoded) { + EnsureOutputSize (outputOffset + TargetEncoding.GetMaxByteCount (charsLeft) + 4, true); + outputLeft = OutputBuffer.Length - outputOffset; + + encoder.Convert (chars, charIndex, charsLeft, OutputBuffer, outputOffset, outputLeft, flush, out nread, out nwritten, out encoded); + outputOffset += nwritten; + charIndex += nread; + charsLeft -= nread; + } + } while (!decoded); + + outputLength = outputOffset; + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + decoder.Reset (); + encoder.Reset (); + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/DecoderFilter.cs b/src/MimeKit/IO/Filters/DecoderFilter.cs new file mode 100644 index 0000000..c85cc49 --- /dev/null +++ b/src/MimeKit/IO/Filters/DecoderFilter.cs @@ -0,0 +1,159 @@ +// +// DecoderFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; +using MimeKit.Encodings; + +namespace MimeKit.IO.Filters { + /// + /// A filter for decoding MIME content. + /// + /// + /// Uses a to incrementally decode data. + /// + public class DecoderFilter : MimeFilterBase + { + /// + /// Gets the decoder used by this filter. + /// + /// + /// Gets the decoder used by this filter. + /// + /// The decoder. + public IMimeDecoder Decoder { + get; private set; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the decoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return Decoder.Encoding; } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new using the specified decoder. + /// + /// A specific decoder for the filter to use. + /// + /// is null. + /// + public DecoderFilter (IMimeDecoder decoder) + { + if (decoder == null) + throw new ArgumentNullException (nameof (decoder)); + + Decoder = decoder; + } + + /// + /// Create a filter that will decode the specified encoding. + /// + /// + /// Creates a new for the specified encoding. + /// + /// A new decoder filter. + /// The encoding to create a filter for. + public static IMimeFilter Create (ContentEncoding encoding) + { + switch (encoding) { + case ContentEncoding.Base64: return new DecoderFilter (new Base64Decoder ()); + case ContentEncoding.QuotedPrintable: return new DecoderFilter (new QuotedPrintableDecoder ()); + case ContentEncoding.UUEncode: return new DecoderFilter (new UUDecoder ()); + default: return new PassThroughFilter (); + } + } + + /// + /// Create a filter that will decode the specified encoding. + /// + /// + /// Creates a new for the specified encoding. + /// + /// A new decoder filter. + /// The name of the encoding to create a filter for. + /// + /// is null. + /// + public static IMimeFilter Create (string name) + { + ContentEncoding encoding; + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!MimeUtils.TryParse (name, out encoding)) + encoding = ContentEncoding.Default; + + return Create (encoding); + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + EnsureOutputSize (Decoder.EstimateOutputLength (length), false); + + outputLength = Decoder.Decode (input, startIndex, length, OutputBuffer); + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + Decoder.Reset (); + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/Dos2UnixFilter.cs b/src/MimeKit/IO/Filters/Dos2UnixFilter.cs new file mode 100644 index 0000000..1497834 --- /dev/null +++ b/src/MimeKit/IO/Filters/Dos2UnixFilter.cs @@ -0,0 +1,121 @@ +// +// Dos2UnixFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.IO.Filters { + /// + /// A filter that will convert from Windows/DOS line endings to Unix line endings. + /// + /// + /// Converts from Windows/DOS line endings to Unix line endings. + /// + public class Dos2UnixFilter : MimeFilterBase + { + readonly bool ensureNewLine; + byte pc; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// Ensure that the stream ends with a new line. + public Dos2UnixFilter (bool ensureNewLine = false) + { + this.ensureNewLine = ensureNewLine; + } + + unsafe int Filter (byte* inbuf, int length, byte* outbuf, bool flush) + { + byte* inend = inbuf + length; + byte* outptr = outbuf; + byte* inptr = inbuf; + + while (inptr < inend) { + if (*inptr == (byte) '\n') { + *outptr++ = *inptr; + } else { + if (pc == (byte) '\r') + *outptr++ = pc; + + if (*inptr != (byte) '\r') + *outptr++ = *inptr; + } + + pc = *inptr++; + } + + if (flush && ensureNewLine && pc != (byte) '\n') + *outptr++ = (byte) '\n'; + + return (int) (outptr - outbuf); + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + if (pc == (byte) '\r') + EnsureOutputSize (length + (flush && ensureNewLine ? 2 : 1), false); + else + EnsureOutputSize (length + (flush && ensureNewLine ? 1 : 0), false); + + outputIndex = 0; + + unsafe { + fixed (byte* inptr = input, outptr = OutputBuffer) { + outputLength = Filter (inptr + startIndex, length, outptr, flush); + } + } + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + pc = 0; + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/EncoderFilter.cs b/src/MimeKit/IO/Filters/EncoderFilter.cs new file mode 100644 index 0000000..8f261bc --- /dev/null +++ b/src/MimeKit/IO/Filters/EncoderFilter.cs @@ -0,0 +1,163 @@ +// +// EncoderFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Utils; +using MimeKit.Encodings; + +namespace MimeKit.IO.Filters { + /// + /// A filter for encoding MIME content. + /// + /// + /// Uses a to incrementally encode data. + /// + public class EncoderFilter : MimeFilterBase + { + /// + /// Gets the encoder used by this filter. + /// + /// + /// Gets the encoder used by this filter. + /// + /// The encoder. + public IMimeEncoder Encoder { + get; private set; + } + + /// + /// Gets the encoding. + /// + /// + /// Gets the encoding that the encoder supports. + /// + /// The encoding. + public ContentEncoding Encoding { + get { return Encoder.Encoding; } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new using the specified encoder. + /// + /// A specific encoder for the filter to use. + /// + /// is null. + /// + public EncoderFilter (IMimeEncoder encoder) + { + if (encoder == null) + throw new ArgumentNullException (nameof (encoder)); + + Encoder = encoder; + } + + /// + /// Create a filter that will encode using specified encoding. + /// + /// + /// Creates a new for the specified encoding. + /// + /// A new encoder filter. + /// The encoding to create a filter for. + public static IMimeFilter Create (ContentEncoding encoding) + { + switch (encoding) { + case ContentEncoding.Base64: return new EncoderFilter (new Base64Encoder ()); + case ContentEncoding.QuotedPrintable: return new EncoderFilter (new QuotedPrintableEncoder ()); + case ContentEncoding.UUEncode: return new EncoderFilter (new UUEncoder ()); + default: return new PassThroughFilter (); + } + } + + /// + /// Create a filter that will encode using specified encoding. + /// + /// + /// Creates a new for the specified encoding. + /// + /// A new encoder filter. + /// The name of the encoding to create a filter for. + /// + /// is null. + /// + public static IMimeFilter Create (string name) + { + ContentEncoding encoding; + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!MimeUtils.TryParse (name, out encoding)) + encoding = ContentEncoding.Default; + + return Create (encoding); + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + EnsureOutputSize (Encoder.EstimateOutputLength (length), false); + + if (flush) + outputLength = Encoder.Flush (input, startIndex, length, OutputBuffer); + else + outputLength = Encoder.Encode (input, startIndex, length, OutputBuffer); + + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + Encoder.Reset (); + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/IMimeFilter.cs b/src/MimeKit/IO/Filters/IMimeFilter.cs new file mode 100644 index 0000000..0154742 --- /dev/null +++ b/src/MimeKit/IO/Filters/IMimeFilter.cs @@ -0,0 +1,74 @@ +// +// IMimeFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.IO.Filters { + /// + /// An interface for incrementally filtering data. + /// + /// + /// An interface for incrementally filtering data. + /// + public interface IMimeFilter + { + /// + /// Filters the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength); + + /// + /// Filters the specified input, flushing all internally buffered data to the output. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + byte[] Flush (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength); + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + void Reset (); + } +} diff --git a/src/MimeKit/IO/Filters/MimeFilterBase.cs b/src/MimeKit/IO/Filters/MimeFilterBase.cs new file mode 100644 index 0000000..4f229b9 --- /dev/null +++ b/src/MimeKit/IO/Filters/MimeFilterBase.cs @@ -0,0 +1,235 @@ +// +// MimeFilterBase.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.IO.Filters { + /// + /// A base implementation for MIME filters. + /// + /// + /// A base implementation for MIME filters. + /// + public abstract class MimeFilterBase : IMimeFilter + { + int preloadLength; + byte[] preload; + byte[] output; + byte[] inbuf; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + protected MimeFilterBase () + { + } + + /// + /// Gets the output buffer. + /// + /// + /// Gets the output buffer. + /// + /// The output buffer. + protected byte[] OutputBuffer { + get { return output; } + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected abstract byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush); + + static int GetIdealBufferSize (int need) + { + return (need + 63) & ~63; + } + + byte[] PreFilter (byte[] input, ref int startIndex, ref int length) + { + if (preloadLength == 0) + return input; + + // We need to preload any data from a previous filter iteration into + // the input buffer, so make sure that we have room... + int totalLength = length + preloadLength; + + if (inbuf == null || inbuf.Length < totalLength) { + // NOTE: Array.Resize() copies data, we don't need that (slower) + inbuf = new byte[GetIdealBufferSize (totalLength)]; + } + + // Copy our preload data into our internal input buffer + Buffer.BlockCopy (preload, 0, inbuf, 0, preloadLength); + + // Copy our input to the end of our internal input buffer + Buffer.BlockCopy (input, startIndex, inbuf, preloadLength, length); + + length = totalLength; + preloadLength = 0; + startIndex = 0; + + return inbuf; + } + + static void ValidateArguments (byte[] input, int startIndex, int length) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException (nameof (startIndex)); + + if (length < 0 || length > (input.Length - startIndex)) + throw new ArgumentOutOfRangeException (nameof (length)); + } + + /// + /// Filters the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + ValidateArguments (input, startIndex, length); + + input = PreFilter (input, ref startIndex, ref length); + + return Filter (input, startIndex, length, out outputIndex, out outputLength, false); + } + + /// + /// Filters the specified input, flushing all internally buffered data to the output. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public byte[] Flush (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + ValidateArguments (input, startIndex, length); + + input = PreFilter (input, ref startIndex, ref length); + + return Filter (input, startIndex, length, out outputIndex, out outputLength, true); + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public virtual void Reset () + { + preloadLength = 0; + } + + /// + /// Saves the remaining input for the next round of processing. + /// + /// + /// Saves the remaining input for the next round of processing. + /// + /// The input buffer. + /// The starting index of the buffer to save. + /// The length of the buffer to save, starting at . + protected void SaveRemainingInput (byte[] input, int startIndex, int length) + { + if (length == 0) + return; + + if (preload == null || preload.Length < length) + preload = new byte[GetIdealBufferSize (length)]; + + Buffer.BlockCopy (input, startIndex, preload, 0, length); + preloadLength = length; + } + + /// + /// Ensures that the output buffer is greater than or equal to the specified size. + /// + /// + /// Ensures that the output buffer is greater than or equal to the specified size. + /// + /// The minimum size needed. + /// If set to true, the current output should be preserved. + protected void EnsureOutputSize (int size, bool keep) + { + int outputSize = output != null ? output.Length : -1; + + if (outputSize >= size) + return; + + if (keep && output != null) + Array.Resize (ref output, GetIdealBufferSize (size)); + else + output = new byte[GetIdealBufferSize (size)]; + } + } +} diff --git a/src/MimeKit/IO/Filters/PassThroughFilter.cs b/src/MimeKit/IO/Filters/PassThroughFilter.cs new file mode 100644 index 0000000..61bc497 --- /dev/null +++ b/src/MimeKit/IO/Filters/PassThroughFilter.cs @@ -0,0 +1,100 @@ +// +// PassThroughFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.IO.Filters { + /// + /// A filter that simply passes data through without any processing. + /// + /// + /// Passes data through without any processing. + /// + public class PassThroughFilter : IMimeFilter + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public PassThroughFilter () + { + } + + #region IMimeFilter implementation + + /// + /// Filters the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + public byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + outputIndex = startIndex; + outputLength = length; + return input; + } + + /// + /// Filters the specified input, flushing all internally buffered data to the output. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes of the input to filter. + /// The starting index of the output in the returned buffer. + /// The length of the output buffer. + public byte[] Flush (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength) + { + outputIndex = startIndex; + outputLength = length; + return input; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public void Reset () + { + } + + #endregion + } +} diff --git a/src/MimeKit/IO/Filters/TrailingWhitespaceFilter.cs b/src/MimeKit/IO/Filters/TrailingWhitespaceFilter.cs new file mode 100644 index 0000000..08916e5 --- /dev/null +++ b/src/MimeKit/IO/Filters/TrailingWhitespaceFilter.cs @@ -0,0 +1,140 @@ +// +// TrailingWhitespaceFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using MimeKit.Utils; + +namespace MimeKit.IO.Filters { + /// + /// A filter for stripping trailing whitespace from lines in a textual stream. + /// + /// + /// Strips trailing whitespace from lines in a textual stream. + /// + public class TrailingWhitespaceFilter : MimeFilterBase + { + readonly PackedByteArray lwsp = new PackedByteArray (); + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public TrailingWhitespaceFilter () + { + } + + unsafe int Filter (byte* inbuf, int length, byte* outbuf) + { + byte* inend = inbuf + length; + byte* outptr = outbuf; + byte* inptr = inbuf; + int count = 0; + + while (inptr < inend) { + if ((*inptr).IsBlank ()) { + lwsp.Add (*inptr); + } else if (*inptr == (byte) '\r') { + *outptr++ = *inptr; + lwsp.Clear (); + count++; + } else if (*inptr == (byte) '\n') { + *outptr++ = *inptr; + lwsp.Clear (); + count++; + } else { + if (lwsp.Count > 0) { + lwsp.CopyTo (OutputBuffer, count); + outptr += lwsp.Count; + count += lwsp.Count; + lwsp.Clear (); + } + + *outptr++ = *inptr; + count++; + } + + inptr++; + } + + return count; + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + if (length == 0) { + if (flush) + lwsp.Clear (); + + outputIndex = startIndex; + outputLength = length; + + return input; + } + + EnsureOutputSize (length + lwsp.Count, false); + + unsafe { + fixed (byte* inptr = input, outptr = OutputBuffer) { + outputLength = Filter (inptr + startIndex, length, outptr); + } + } + + if (flush) + lwsp.Clear (); + + outputIndex = 0; + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + lwsp.Clear (); + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/Filters/Unix2DosFilter.cs b/src/MimeKit/IO/Filters/Unix2DosFilter.cs new file mode 100644 index 0000000..d70f31e --- /dev/null +++ b/src/MimeKit/IO/Filters/Unix2DosFilter.cs @@ -0,0 +1,120 @@ +// +// Unix2DosFilter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.IO.Filters { + /// + /// A filter that will convert from Unix line endings to Windows/DOS line endings. + /// + /// + /// Converts from Unix line endings to Windows/DOS line endings. + /// + public class Unix2DosFilter : MimeFilterBase + { + readonly bool ensureNewLine; + byte pc; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// Ensure that the stream ends with a new line. + public Unix2DosFilter (bool ensureNewLine = false) + { + this.ensureNewLine = ensureNewLine; + } + + unsafe int Filter (byte* inbuf, int length, byte* outbuf, bool flush) + { + byte* inend = inbuf + length; + byte* outptr = outbuf; + byte* inptr = inbuf; + + while (inptr < inend) { + if (*inptr == (byte) '\r') { + *outptr++ = *inptr; + } else if (*inptr == (byte) '\n') { + if (pc != (byte) '\r') + *outptr++ = (byte) '\r'; + *outptr++ = *inptr; + } else { + *outptr++ = *inptr; + } + + pc = *inptr++; + } + + if (flush && ensureNewLine && pc != (byte) '\n') { + *outptr++ = (byte) '\r'; + *outptr++ = (byte) '\n'; + } + + return (int) (outptr - outbuf); + } + + /// + /// Filter the specified input. + /// + /// + /// Filters the specified input buffer starting at the given index, + /// spanning across the specified number of bytes. + /// + /// The filtered output. + /// The input buffer. + /// The starting index of the input buffer. + /// The length of the input buffer, starting at . + /// The output index. + /// The output length. + /// If set to true, all internally buffered data should be flushed to the output buffer. + protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush) + { + EnsureOutputSize (length * 2 + (flush && ensureNewLine ? 2 : 0), false); + + outputIndex = 0; + + unsafe { + fixed (byte* inptr = input, outptr = OutputBuffer) { + outputLength = Filter (inptr + startIndex, length, outptr, flush); + } + } + + return OutputBuffer; + } + + /// + /// Resets the filter. + /// + /// + /// Resets the filter. + /// + public override void Reset () + { + pc = 0; + base.Reset (); + } + } +} diff --git a/src/MimeKit/IO/ICancellableStream.cs b/src/MimeKit/IO/ICancellableStream.cs new file mode 100644 index 0000000..2fa2c6d --- /dev/null +++ b/src/MimeKit/IO/ICancellableStream.cs @@ -0,0 +1,95 @@ +// +// ICancellableStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Threading; + +namespace MimeKit.IO { + /// + /// An interface allowing for a cancellable stream reading operation. + /// + /// + /// This interface is meant to extend the functionality of a , + /// allowing the to have much finer-grained canellability. + /// When a custom stream implementation also implements this interface, + /// the will opt to use this interface + /// instead of the normal + /// API to read data from the stream. + /// This is really useful when parsing a message or other MIME entity + /// directly from a network-based stream. + /// + public interface ICancellableStream + { + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// When a custom stream implementation also implements this interface, + /// the will opt to use this interface + /// instead of the normal + /// API to read data from the stream. + /// This is really useful when parsing a message or other MIME entity + /// directly from a network-based stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many + /// bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken); + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// When a custom stream implementation also implements this interface, + /// writing a or + /// to the custom stream will opt to use this interface + /// instead of the normal + /// API to write data to the stream. + /// This is really useful when writing a message or other MIME entity + /// directly to a network-based stream. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token + void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken); + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// The cancellation token. + void Flush (CancellationToken cancellationToken); + } +} diff --git a/src/MimeKit/IO/MeasuringStream.cs b/src/MimeKit/IO/MeasuringStream.cs new file mode 100644 index 0000000..6ce1c70 --- /dev/null +++ b/src/MimeKit/IO/MeasuringStream.cs @@ -0,0 +1,427 @@ +// +// MeasuringStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MimeKit.IO { + /// + /// A stream useful for measuring the amount of data written. + /// + /// + /// A keeps track of the number of bytes + /// that have been written to it. This is useful, for example, when you + /// need to know how large a is without + /// actually writing it to disk or into a memory buffer. + /// + public class MeasuringStream : Stream + { + bool disposed; + long position; + long length; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public MeasuringStream () + { + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (MeasuringStream)); + } + + #region implemented abstract members of Stream + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// A is not readable. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return false; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// A is always writable. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return true; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// A is always seekable. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return true; } + } + + /// + /// Checks whether or not reading and writing to the stream can timeout. + /// + /// + /// Writing to a cannot timeout. + /// + /// true if reading and writing to the stream can timeout; otherwise, false. + public override bool CanTimeout { + get { return false; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// The length of a indicates the + /// number of bytes that have been written to it. + /// + /// The length of the stream in bytes. + /// + /// The stream has been disposed. + /// + public override long Length { + get { + CheckDisposed (); + + return length; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// Since it is possible to seek within a , + /// it is possible that the position will not always be identical to the + /// length of the stream, but typically it will be. + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return position; } + set { Seek (value, SeekOrigin.Begin); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reading from a is not supported. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support reading"); + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reading from a is not supported. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support reading. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + CheckDisposed (); + + throw new NotSupportedException ("The stream does not support reading"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Increments the property by the number of bytes written. + /// If the updated position is greater than the current length of the stream, then + /// the property will be updated to be identical to the + /// position. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + position += count; + + length = Math.Max (length, position); + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Increments the property by the number of bytes written. + /// If the updated position is greater than the current length of the stream, then + /// the property will be updated to be identical to the + /// position. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Write (buffer, offset, count); + + return Task.FromResult (0); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Updates the within the stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + public override long Seek (long offset, SeekOrigin origin) + { + long real; + + CheckDisposed (); + + switch (origin) { + case SeekOrigin.Begin: + real = offset; + break; + case SeekOrigin.Current: + real = position + offset; + break; + case SeekOrigin.End: + real = length + offset; + break; + default: + throw new ArgumentOutOfRangeException (nameof (origin), "Invalid SeekOrigin specified"); + } + + // sanity check the resultant offset + if (real < 0) + throw new IOException ("Cannot seek to a position before the beginning of the stream"); + + // short-cut if we are seeking to our current position + if (real == position) + return position; + + if (real > length) + throw new IOException ("Cannot seek beyond the end of the stream"); + + position = real; + + return position; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Since a does not actually do anything other than + /// count bytes, this method is a no-op. + /// + /// + /// The stream has been disposed. + /// + public override void Flush () + { + CheckDisposed (); + + // nothing to do... + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// Since a does not actually do anything other than + /// count bytes, this method is a no-op. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + // nothing to do... + return Task.FromResult (0); + } + + /// + /// Sets the length of the stream. + /// + /// + /// Sets the to the specified value and updates + /// to the specified value if (and only if) + /// the current position is greater than the new length value. + /// + /// The desired length of the stream in bytes. + /// + /// is out of range. + /// + /// + /// The stream has been disposed. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + if (value < 0) + throw new ArgumentOutOfRangeException (nameof (value)); + + position = Math.Min (position, value); + length = value; + } + + #endregion + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + disposed = true; + } + } +} diff --git a/src/MimeKit/IO/MemoryBlockStream.cs b/src/MimeKit/IO/MemoryBlockStream.cs new file mode 100644 index 0000000..0f3e6a0 --- /dev/null +++ b/src/MimeKit/IO/MemoryBlockStream.cs @@ -0,0 +1,557 @@ +// +// MemoryBlockStream.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics; + +using MimeKit.Utils; + +namespace MimeKit.IO { + /// + /// An efficient memory stream implementation that sacrifices the ability to + /// get access to the internal byte buffer in order to drastically improve + /// performance. + /// + /// + /// Instead of resizing an internal byte array, the + /// chains blocks of non-contiguous memory. This helps improve performance by avoiding + /// unneeded copying of data from the old array to the newly allocated array as well + /// as the zeroing of the newly allocated array. + /// + public class MemoryBlockStream : Stream + { + const long MaxCapacity = int.MaxValue * BlockSize; + const long BlockSize = 2048; + + static readonly BufferPool DefaultPool = new BufferPool ((int) BlockSize, 200); + + readonly List blocks = new List (); + readonly BufferPool pool; + long position, length; + bool disposed; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with an initial memory block + /// of 2048 bytes. + /// + public MemoryBlockStream () + { + pool = DefaultPool; + blocks.Add (pool.Rent (Debugger.IsAttached)); + } + + /// + /// Copies the memory stream into a byte array. + /// + /// + /// Copies all of the stream data into a newly allocated byte array. + /// + /// The array. + public byte[] ToArray () + { + var array = new byte[length]; + int need = (int) length; + int arrayIndex = 0; + int nread = 0; + int block = 0; + + while (nread < length) { + int n = Math.Min ((int) BlockSize, need); + Buffer.BlockCopy (blocks[block], 0, array, arrayIndex, n); + arrayIndex += n; + nread += n; + need -= n; + block++; + } + + return array; + } + + void CheckDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (MemoryBlockStream)); + } + + #region implemented abstract members of Stream + + /// + /// Checks whether or not the stream supports reading. + /// + /// + /// The is always readable. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return true; } + } + + /// + /// Checks whether or not the stream supports writing. + /// + /// + /// The is always writable. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return true; } + } + + /// + /// Checks whether or not the stream supports seeking. + /// + /// + /// The is always seekable. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return true; } + } + + /// + /// Checks whether or not reading and writing to the stream can timeout. + /// + /// + /// The does not support timing out. + /// + /// true if reading and writing to the stream can timeout; otherwise, false. + public override bool CanTimeout { + get { return false; } + } + + /// + /// Gets the length of the stream, in bytes. + /// + /// + /// Gets the length of the stream, in bytes. + /// + /// The length of the stream, in bytes. + /// + /// The stream has been disposed. + /// + public override long Length { + get { + CheckDisposed (); + + return length; + } + } + + /// + /// Gets or sets the current position within the stream. + /// + /// + /// Gets or sets the current position within the stream. + /// + /// The position of the stream. + /// + /// An I/O error occurred. + /// + /// + /// The stream does not support seeking. + /// + /// + /// The stream has been disposed. + /// + public override long Position { + get { return position; } + set { Seek (value, SeekOrigin.Begin); } + } + + static void ValidateArguments (byte[] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException (nameof (buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException (nameof (offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException (nameof (count)); + } + + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override int Read (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (position == MaxCapacity) + return 0; + + int max = Math.Min ((int) (length - position), count); + int startIndex = (int) (position % BlockSize); + int block = (int) (position / BlockSize); + int nread = 0; + + while (nread < max && block < blocks.Count) { + int n = Math.Min ((int) BlockSize - startIndex, max - nread); + Buffer.BlockCopy (blocks[block], startIndex, buffer, offset + nread, n); + startIndex = 0; + nread += n; + block++; + } + + position += nread; + + return nread; + } + + /// + /// Asynchronously reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// + /// Reads a sequence of bytes from the stream and advances the position + /// within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// The buffer to read data into. + /// The offset into the buffer to start reading data. + /// The number of bytes to read. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override Task ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.FromResult (Read (buffer, offset, count)); + } + + /// + /// Writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes the entire buffer to the stream and advances the current position + /// within the stream by the number of bytes written, adding memory blocks as + /// needed in order to contain the newly written bytes. + /// + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override void Write (byte[] buffer, int offset, int count) + { + CheckDisposed (); + + ValidateArguments (buffer, offset, count); + + if (position + count >= MaxCapacity) + throw new IOException (string.Format ("Cannot exceed {0} bytes", MaxCapacity)); + + int startIndex = (int) (position % BlockSize); + long capacity = blocks.Count * BlockSize; + int block = (int) (position / BlockSize); + int nwritten = 0; + + while (capacity < position + count) { + blocks.Add (pool.Rent (Debugger.IsAttached)); + capacity += BlockSize; + } + + while (nwritten < count) { + int n = Math.Min ((int) BlockSize - startIndex, count - nwritten); + Buffer.BlockCopy (buffer, offset + nwritten, blocks[block], startIndex, n); + startIndex = 0; + nwritten += n; + block++; + } + + position += nwritten; + + length = Math.Max (length, position); + } + + /// + /// Asynchronously writes a sequence of bytes to the stream and advances the current + /// position within this stream by the number of bytes written. + /// + /// + /// Writes the entire buffer to the stream and advances the current position + /// within the stream by the number of bytes written, adding memory blocks as + /// needed in order to contain the newly written bytes. + /// + /// A task that represents the asynchronous write operation. + /// The buffer to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is less than zero or greater than the length of . + /// -or- + /// The is not large enough to contain bytes starting + /// at the specified . + /// + /// + /// The stream has been disposed. + /// + /// + /// The stream does not support writing. + /// + /// + /// An I/O error occurred. + /// + public override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Write (buffer, offset, count); + + return Task.FromResult (0); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// Sets the position within the current stream. + /// + /// The new position within the stream. + /// The offset into the stream relative to the . + /// The origin to seek from. + /// + /// is not a valid . + /// + /// + /// The stream has been disposed. + /// + /// + /// An I/O error occurred. + /// + public override long Seek (long offset, SeekOrigin origin) + { + long real; + + CheckDisposed (); + + switch (origin) { + case SeekOrigin.Begin: + real = offset; + break; + case SeekOrigin.Current: + real = position + offset; + break; + case SeekOrigin.End: + real = length + offset; + break; + default: + throw new ArgumentOutOfRangeException (nameof (origin), "Invalid SeekOrigin specified"); + } + + // sanity check the resultant offset + if (real < 0) + throw new IOException ("Cannot seek to a position before the beginning of the stream"); + + if (real > MaxCapacity) + throw new IOException (string.Format ("Cannot exceed {0} bytes", MaxCapacity)); + + // short-cut if we are seeking to our current position + if (real == position) + return position; + + // TODO: MemoryStream allows seeking past the end - should MemoryBlockStream? + if (real > length) + throw new IOException ("Cannot seek beyond the end of the stream"); + + position = real; + + return position; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// This method does not do anything. + /// + /// + /// The stream has been disposed. + /// + public override void Flush () + { + CheckDisposed (); + + // nothing to do... + } + + /// + /// Asynchronously clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + /// + /// This method does not do anything. + /// + /// A task that represents the asynchronous flush operation. + /// The cancellation token. + /// + /// The stream has been disposed. + /// + public override Task FlushAsync (CancellationToken cancellationToken) + { + CheckDisposed (); + + return Task.FromResult (0); + } + + /// + /// Sets the length of the stream. + /// + /// + /// Sets the length of the stream. + /// + /// The desired length of the stream in bytes. + /// + /// is out of range. + /// + /// + /// The stream has been disposed. + /// + public override void SetLength (long value) + { + CheckDisposed (); + + if (value < 0 || value > MaxCapacity) + throw new ArgumentOutOfRangeException (nameof (value)); + + long capacity = blocks.Count * BlockSize; + + if (value > capacity) { + do { + blocks.Add (pool.Rent (Debugger.IsAttached)); + capacity += BlockSize; + } while (capacity < value); + } else if (value < length) { + // shed any blocks that are no longer needed + while (capacity - value > BlockSize) { + pool.Return (blocks[blocks.Count - 1]); + blocks.RemoveAt (blocks.Count - 1); + capacity -= BlockSize; + } + + // reset the range of bytes between the new length and the old length to 0 + int count = (int) (Math.Min (length, capacity) - value); + int startIndex = (int) (value % BlockSize); + int block = (int) (value / BlockSize); + + Array.Clear (blocks[block], startIndex, count); + } + + position = Math.Min (position, value); + length = value; + } + + #endregion + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected override void Dispose (bool disposing) + { + if (disposing && !disposed) { + for (int i = 0; i < blocks.Count; i++) { + pool.Return (blocks[i]); + blocks[i] = null; + } + + blocks.Clear (); + disposed = true; + } + + base.Dispose (disposing); + } + } +} diff --git a/src/MimeKit/InternetAddress.cs b/src/MimeKit/InternetAddress.cs new file mode 100644 index 0000000..d71fc2b --- /dev/null +++ b/src/MimeKit/InternetAddress.cs @@ -0,0 +1,1361 @@ +// +// InternetAddress.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// An abstract internet address, as specified by rfc0822. + /// + /// + /// A can be any type of address defined by the + /// original Internet Message specification. + /// There are effectively two (2) types of addresses: mailboxes and groups. + /// Mailbox addresses are what are most commonly known as email addresses and are + /// represented by the class. + /// Group addresses are themselves lists of addresses and are represented by the + /// class. While rare, it is still important to handle these + /// types of addresses. They typically only contain mailbox addresses, but may also + /// contain other group addresses. + /// + public abstract class InternetAddress : IComparable, IEquatable + { + const string AtomSpecials = "()<>@,;:\\\".[]"; + Encoding encoding; + string name; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes the and properties of the internet address. + /// + /// The character encoding to be used for encoding the name. + /// The name of the mailbox or group. + /// + /// is null. + /// + protected InternetAddress (Encoding encoding, string name) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + Encoding = encoding; + Name = name; + } + + /// + /// Get or set the character encoding to use when encoding the name of the address. + /// + /// + /// The character encoding is used to convert the property, if it is set, + /// to a stream of bytes when encoding the internet address for transport. + /// + /// The character encoding. + /// + /// is null. + /// + public Encoding Encoding { + get { return encoding; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value == encoding) + return; + + encoding = value; + OnChanged (); + } + } + + /// + /// Get or set the display name of the address. + /// + /// + /// A name is optional and is typically set to the name of the person + /// or group that own the internet address. + /// + /// The name of the address. + public string Name { + get { return name; } + set { + if (value == name) + return; + + name = value; + OnChanged (); + } + } + + /// + /// Clone the address. + /// + /// + /// Clones the address. + /// + /// The cloned address. + public abstract InternetAddress Clone (); + + #region IComparable implementation + + /// + /// Compares two internet addresses. + /// + /// + /// Compares two internet addresses for the purpose of sorting. + /// + /// The sort order of the current internet address compared to the other internet address. + /// The internet address to compare to. + /// + /// is null. + /// + public int CompareTo (InternetAddress other) + { + int rv; + + if (other == null) + throw new ArgumentNullException (nameof (other)); + + if ((rv = string.Compare (Name, other.Name, StringComparison.OrdinalIgnoreCase)) != 0) + return rv; + + var otherMailbox = other as MailboxAddress; + var mailbox = this as MailboxAddress; + + if (mailbox != null && otherMailbox != null) { + string otherAddress = otherMailbox.Address; + int otherAt = otherAddress.IndexOf ('@'); + string address = mailbox.Address; + int at = address.IndexOf ('@'); + + if (at != -1 && otherAt != -1) { + int length = Math.Min (address.Length - (at + 1), otherAddress.Length - (otherAt + 1)); + + rv = string.Compare (address, at + 1, otherAddress, otherAt + 1, length, StringComparison.OrdinalIgnoreCase); + } + + if (rv == 0) { + string otherUser = otherAt != -1 ? otherAddress.Substring (0, otherAt) : otherAddress; + string user = at != -1 ? address.Substring (0, at) : address; + + rv = string.Compare (user, otherUser, StringComparison.OrdinalIgnoreCase); + } + + return rv; + } + + // sort mailbox addresses before group addresses + if (mailbox != null && otherMailbox == null) + return -1; + + if (mailbox == null && otherMailbox != null) + return 1; + + return 0; + } + + #endregion + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Compares two internet addresses to determine if they are identical or not. + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public abstract bool Equals (InternetAddress other); + + #endregion + + /// + /// Determine whether the specified object is equal to the current object. + /// + /// + /// The type of comparison between the current instance and the parameter depends on whether + /// the current instance is a reference type or a value type. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals (object obj) + { + return Equals (obj as InternetAddress); + } + + /// + /// Return the hash code for this instance. + /// + /// + /// Returns the hash code for this instance. + /// + /// A hash code for the current object. + public override int GetHashCode () + { + return ToString ().GetHashCode (); + } + + internal static string EncodeInternationalizedPhrase (string phrase) + { + for (int i = 0; i < phrase.Length; i++) { + if (AtomSpecials.IndexOf (phrase[i]) != -1) + return MimeUtils.Quote (phrase); + } + + return phrase; + } + + internal abstract void Encode (FormatOptions options, StringBuilder builder, bool firstToken, ref int lineLength); + + /// + /// Serialize an to a string, optionally encoding it for transport. + /// + /// + /// If the parameter is true, then this method will return + /// an encoded version of the internet address according to the rules described in rfc2047. + /// However, if the parameter is false, then this method will + /// return a string suitable only for display purposes. + /// + /// A string representing the . + /// The formatting options. + /// If set to true, the will be encoded. + /// + /// is null. + /// + public abstract string ToString (FormatOptions options, bool encode); + + /// + /// Serialize an to a string, optionally encoding it for transport. + /// + /// + /// If the parameter is true, then this method will return + /// an encoded version of the internet address according to the rules described in rfc2047. + /// However, if the parameter is false, then this method will + /// return a string suitable only for display purposes. + /// + /// A string representing the . + /// If set to true, the will be encoded. + public string ToString (bool encode) + { + return ToString (FormatOptions.Default, encode); + } + + /// + /// Serialize an to a string suitable for display. + /// + /// + /// The string returned by this method is suitable only for display purposes. + /// + /// A string representing the . + public override string ToString () + { + return ToString (FormatOptions.Default, false); + } + + internal event EventHandler Changed; + + /// + /// Raise the internal changed event used by to keep headers in sync. + /// + /// + /// This method is called whenever a property of the internet address is changed. + /// + protected virtual void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + internal static bool TryParseLocalPart (byte[] text, ref int index, int endIndex, bool skipTrailingCfws, bool throwOnError, out string localpart) + { + var token = new StringBuilder (); + int startIndex = index; + + localpart = null; + + do { + if (!text[index].IsAtom () && text[index] != '"') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid local-part at offset {0}", startIndex), startIndex, index); + + return false; + } + + int start = index; + if (!ParseUtils.SkipWord (text, ref index, endIndex, throwOnError)) + return false; + + try { + token.Append (CharsetUtils.UTF8.GetString (text, start, index - start)); + } catch (DecoderFallbackException) { + try { + token.Append (CharsetUtils.Latin1.GetString (text, start, index - start)); + } catch (DecoderFallbackException ex) { + if (throwOnError) + throw new ParseException ("Internationalized local-part tokens may only contain UTF-8 characters.", start, start, ex); + + return false; + } + } + + int cfws = index; + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || text[index] != (byte) '.') { + if (!skipTrailingCfws) + index = cfws; + break; + } + + token.Append ('.'); + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete local-part at offset {0}", startIndex), startIndex, index); + + return false; + } + } while (true); + + localpart = token.ToString (); + + if (ParseUtils.IsIdnEncoded (localpart)) + localpart = ParseUtils.IdnDecode (localpart); + + return true; + } + + static readonly byte[] CommaGreaterThanOrSemiColon = { (byte) ',', (byte) '>', (byte) ';' }; + + internal static bool TryParseAddrspec (byte[] text, ref int index, int endIndex, byte[] sentinels, bool throwOnError, out string addrspec, out int at) + { + int startIndex = index; + string localpart; + + addrspec = null; + at = -1; + + if (!TryParseLocalPart (text, ref index, endIndex, true, throwOnError, out localpart)) + return false; + + if (index >= endIndex || ParseUtils.IsSentinel (text[index], sentinels)) { + addrspec = localpart; + return true; + } + + if (text[index] != (byte) '@') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid addr-spec token at offset {0}", startIndex), startIndex, index); + + return false; + } + + index++; + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete addr-spec token at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete addr-spec token at offset {0}", startIndex), startIndex, index); + + return false; + } + + string domain; + if (!ParseUtils.TryParseDomain (text, ref index, endIndex, sentinels, throwOnError, out domain)) + return false; + + if (ParseUtils.IsIdnEncoded (domain)) + domain = ParseUtils.IdnDecode (domain); + + addrspec = localpart + "@" + domain; + at = localpart.Length; + + return true; + } + + internal static bool TryParseMailbox (ParserOptions options, byte[] text, int startIndex, ref int index, int endIndex, string name, int codepage, bool throwOnError, out InternetAddress address) + { + DomainList route = null; + Encoding encoding; + + try { + encoding = Encoding.GetEncoding (codepage); + } catch { + encoding = Encoding.UTF8; + } + + address = null; + + // skip over the '<' + index++; + + // Note: check for excessive angle brackets like the example described in section 7.1.2 of rfc7103... + if (index < endIndex && text[index] == (byte) '<') { + if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) { + if (throwOnError) + throw new ParseException (string.Format ("Excessive angle brackets at offset {0}", index), startIndex, index); + + return false; + } + + do { + index++; + } while (index < endIndex && text[index] == '<'); + } + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete mailbox at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (text[index] == (byte) '@') { + // Note: we always pass 'false' as the throwOnError argument here so that we can throw a more informative exception on error + if (!DomainList.TryParse (text, ref index, endIndex, false, out route)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid route in mailbox at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (index >= endIndex || text[index] != (byte) ':') { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete route in mailbox at offset {0}", startIndex), startIndex, index); + + return false; + } + + // skip over ':' + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete mailbox at offset {0}", startIndex), startIndex, index); + + return false; + } + } + + // Note: The only syntactically correct sentinel token here is the '>', but alas... to deal with the first example + // in section 7.1.5 of rfc7103, we need to at least handle ',' as a sentinel and might as well handle ';' as well + // in case the mailbox is within a group address. + // + // Example: + string addrspec; + int at; + + if (!TryParseAddrspec (text, ref index, endIndex, CommaGreaterThanOrSemiColon, throwOnError, out addrspec, out at)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || text[index] != (byte) '>') { + if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected end of mailbox at offset {0}", startIndex), startIndex, index); + + return false; + } + } else { + // skip over the '>' + index++; + + // Note: check for excessive angle brackets like the example described in section 7.1.2 of rfc7103... + if (index < endIndex && text[index] == (byte) '>') { + if (options.AddressParserComplianceMode == RfcComplianceMode.Strict) { + if (throwOnError) + throw new ParseException (string.Format ("Excessive angle brackets at offset {0}", index), startIndex, index); + + return false; + } + + do { + index++; + } while (index < endIndex && text[index] == '>'); + } + } + + if (route != null) + address = new MailboxAddress (encoding, name, route, addrspec, at); + else + address = new MailboxAddress (encoding, name, addrspec, at); + + return true; + } + + static bool TryParseGroup (ParserOptions options, byte[] text, int startIndex, ref int index, int endIndex, int groupDepth, string name, int codepage, bool throwOnError, out InternetAddress address) + { + List members; + Encoding encoding; + + try { + encoding = Encoding.GetEncoding (codepage); + } catch { + encoding = Encoding.UTF8; + } + + address = null; + + // skip over the ':' + index++; + + while (index < endIndex && (text[index] == ':' || text[index].IsBlank ())) + index++; + + if (InternetAddressList.TryParse (options, text, ref index, endIndex, true, groupDepth, throwOnError, out members)) + address = new GroupAddress (encoding, name, members); + else + address = new GroupAddress (encoding, name); + + if (index >= endIndex || text[index] != (byte) ';') { + if (throwOnError && options.AddressParserComplianceMode == RfcComplianceMode.Strict) + throw new ParseException (string.Format ("Expected to find ';' at offset {0}", index), startIndex, index); + + while (index < endIndex && text[index] != (byte) ';') + index++; + } else { + index++; + } + + return true; + } + + [Flags] + internal enum AddressParserFlags { + AllowMailboxAddress = 1 << 0, + AllowGroupAddress = 1 << 1, + ThrowOnError = 1 << 2, + + TryParse = AllowMailboxAddress | AllowGroupAddress, + Parse = TryParse | ThrowOnError + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, int groupDepth, AddressParserFlags flags, out InternetAddress address) + { + bool strict = options.AddressParserComplianceMode == RfcComplianceMode.Strict; + bool throwOnError = (flags & AddressParserFlags.ThrowOnError) != 0; + int minWordCount = options.AllowUnquotedCommasInAddresses ? 0 : 1; + + address = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index == endIndex) { + if (throwOnError) + throw new ParseException ("No address found.", index, index); + + return false; + } + + // keep track of the start & length of the phrase + bool trimLeadingQuote = false; + int startIndex = index; + int length = 0; + int words = 0; + + while (index < endIndex) { + if (strict) { + if (!ParseUtils.SkipWord (text, ref index, endIndex, throwOnError)) + break; + } else if (text[index] == (byte) '"') { + int qstringIndex = index; + + if (!ParseUtils.SkipQuoted (text, ref index, endIndex, false)) { + index = qstringIndex + 1; + + ParseUtils.SkipWhiteSpace (text, ref index, endIndex); + + if (!ParseUtils.SkipPhraseAtom (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete quoted-string token at offset {0}", qstringIndex), qstringIndex, endIndex); + + break; + } + + if (startIndex == qstringIndex) + trimLeadingQuote = true; + } + } else { + if (!ParseUtils.SkipPhraseAtom (text, ref index, endIndex)) + break; + } + + length = index - startIndex; + + do { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + // Note: some clients don't quote dots in the name + if (index >= endIndex || text[index] != (byte) '.') + break; + + index++; + + length = index - startIndex; + } while (true); + + words++; + + // Note: some clients don't quote commas in the name + if (index < endIndex && text[index] == ',' && words > minWordCount) { + index++; + + length = index - startIndex; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + } + } + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + // specials = "(" / ")" / "<" / ">" / "@" ; Must be in quoted- + // / "," / ";" / ":" / "\" / <"> ; string, to use + // / "." / "[" / "]" ; within a word. + + if (index >= endIndex || text[index] == (byte) ',' || text[index] == (byte) '>' || text[index] == ';') { + // we've completely gobbled up an addr-spec w/o a domain + byte sentinel = index < endIndex ? text[index] : (byte) ','; + string name, addrspec; + + if ((flags & AddressParserFlags.AllowMailboxAddress) == 0) { + if (throwOnError) + throw new ParseException (string.Format ("Addr-spec token at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (!options.AllowAddressesWithoutDomain) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete addr-spec token at offset {0}", startIndex), startIndex, index); + + return false; + } + + // rewind back to the beginning of the local-part + index = startIndex; + + if (!TryParseLocalPart (text, ref index, endIndex, false, throwOnError, out addrspec)) + return false; + + ParseUtils.SkipWhiteSpace (text, ref index, endIndex); + + if (index < endIndex && text[index] == '(') { + int comment = index + 1; + + // Note: this can't fail because it has already been skipped in TryParseLocalPart() above. + ParseUtils.SkipComment (text, ref index, endIndex); + + name = Rfc2047.DecodePhrase (options, text, comment, (index - 1) - comment).Trim (); + + ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError); + } else { + name = string.Empty; + } + + if (index < endIndex && text[index] == (byte) '>') { + if (strict) { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected '>' token at offset {0}", index), startIndex, index); + + return false; + } + + index++; + } + + if (index < endIndex && text[index] != sentinel) { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), startIndex, index); + + return false; + } + + address = new MailboxAddress (Encoding.UTF8, name, addrspec, -1); + + return true; + } + + if (text[index] == (byte) ':') { + // rfc2822 group address + int nameIndex = startIndex; + int codepage = -1; + string name; + + if ((flags & AddressParserFlags.AllowGroupAddress) == 0) { + if (throwOnError) + throw new ParseException (string.Format ("Group address token at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (groupDepth >= options.MaxAddressGroupDepth) { + if (throwOnError) + throw new ParseException (string.Format ("Exceeded maximum rfc822 group depth at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (trimLeadingQuote) { + nameIndex++; + length--; + } + + if (length > 0) { + name = Rfc2047.DecodePhrase (options, text, nameIndex, length, out codepage); + } else { + name = string.Empty; + } + + if (codepage == -1) + codepage = 65001; + + return TryParseGroup (options, text, startIndex, ref index, endIndex, groupDepth + 1, MimeUtils.Unquote (name), codepage, throwOnError, out address); + } + + if ((flags & AddressParserFlags.AllowMailboxAddress) == 0) { + if (throwOnError) + throw new ParseException (string.Format ("Mailbox address token at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (text[index] == (byte) '@') { + // we're either in the middle of an addr-spec token or we completely gobbled up an addr-spec w/o a domain + string name, addrspec; + int at; + + // rewind back to the beginning of the local-part + index = startIndex; + + if (!TryParseAddrspec (text, ref index, endIndex, CommaGreaterThanOrSemiColon, throwOnError, out addrspec, out at)) + return false; + + ParseUtils.SkipWhiteSpace (text, ref index, endIndex); + + if (index < endIndex && text[index] == '(') { + int comment = index; + + if (!ParseUtils.SkipComment (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete comment token at offset {0}", comment), comment, index); + + return false; + } + + comment++; + + name = Rfc2047.DecodePhrase (options, text, comment, (index - 1) - comment).Trim (); + } else { + name = string.Empty; + } + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + address = new MailboxAddress (Encoding.UTF8, name, addrspec, at); + return true; + } + + if (text[index] == (byte) '<') { + // We have an address like "user@example.com "; i.e. the name is an unquoted string with an '@'. + if (strict) { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected '<' token at offset {0}", index), startIndex, index); + + return false; + } + + int nameEndIndex = index; + while (nameEndIndex > startIndex && text[nameEndIndex - 1].IsWhitespace ()) + nameEndIndex--; + + length = nameEndIndex - startIndex; + + // fall through to the rfc822 angle-addr token case... + } else { + // Note: since there was no '<', there should not be a '>'... but we handle it anyway in order to + // deal with the second Unbalanced Angle Brackets example in section 7.1.3: second@example.org> + if (text[index] == (byte) '>') { + if (strict) { + if (throwOnError) + throw new ParseException (string.Format ("Unexpected '>' token at offset {0}", index), startIndex, index); + + return false; + } + + index++; + } + + address = new MailboxAddress (Encoding.UTF8, name, addrspec, at); + + return true; + } + } + + if (text[index] == (byte) '<') { + // rfc2822 angle-addr token + int nameIndex = startIndex; + int codepage = -1; + string name; + + if (trimLeadingQuote) { + nameIndex++; + length--; + } + + if (length > 0) { + name = Rfc2047.DecodePhrase (options, text, nameIndex, length, out codepage); + } else { + name = string.Empty; + } + + if (codepage == -1) + codepage = 65001; + + return TryParseMailbox (options, text, startIndex, ref index, endIndex, MimeUtils.Unquote (name), codepage, throwOnError, out address); + } + + if (throwOnError) + throw new ParseException (string.Format ("Invalid address token at offset {0}", startIndex), startIndex, index); + + return false; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out InternetAddress address) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.TryParse, out address)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false)) { + address = null; + return false; + } + + if (index != endIndex) { + address = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed address. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out InternetAddress address) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out address); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out InternetAddress address) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.TryParse, out address)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + address = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed address. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out InternetAddress address) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out address); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The parsed address. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out InternetAddress address) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.TryParse, out address)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + address = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The parsed address. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out InternetAddress address) + { + return TryParse (ParserOptions.Default, buffer, out address); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single or . If the text contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The text. + /// The parsed address. + /// + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out InternetAddress address) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.TryParse, out address)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + address = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single or . If the text contains + /// more data, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The text. + /// The parsed address. + /// + /// is null. + /// + public static bool TryParse (string text, out InternetAddress address) + { + return TryParse (ParserOptions.Default, text, out address); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + InternetAddress address; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.Parse, out address)) + throw new ParseException ("No address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return address; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + InternetAddress address; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.Parse, out address)) + throw new ParseException ("No address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return address; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + InternetAddress address; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.Parse, out address)) + throw new ParseException ("No address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return address; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single or . If the buffer contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single or . If the text contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (ParserOptions options, string text) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + InternetAddress address; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, 0, AddressParserFlags.Parse, out address)) + throw new ParseException ("No address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return address; + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single or . If the text contains + /// more data, then parsing will fail. + /// + /// The parsed . + /// The text. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddress Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + } +} diff --git a/src/MimeKit/InternetAddressList.cs b/src/MimeKit/InternetAddressList.cs new file mode 100644 index 0000000..e989bfc --- /dev/null +++ b/src/MimeKit/InternetAddressList.cs @@ -0,0 +1,1110 @@ +// +// InternetAddressList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +#if ENABLE_SNM +using System.Net.Mail; +#endif + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A list of email addresses. + /// + /// + /// An may contain any number of addresses of any type + /// defined by the original Internet Message specification. + /// There are effectively two (2) types of addresses: mailboxes and groups. + /// Mailbox addresses are what are most commonly known as email addresses and are + /// represented by the class. + /// Group addresses are themselves lists of addresses and are represented by the + /// class. While rare, it is still important to handle these + /// types of addresses. They typically only contain mailbox addresses, but may also + /// contain other group addresses. + /// + public class InternetAddressList : IList, IEquatable, IComparable + { + readonly List list = new List (); + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new containing the supplied addresses. + /// + /// An initial list of addresses. + /// + /// is null. + /// + public InternetAddressList (IEnumerable addresses) + { + if (addresses == null) + throw new ArgumentNullException (nameof (addresses)); + + foreach (var address in addresses) { + address.Changed += AddressChanged; + list.Add (address); + } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new, empty, . + /// + public InternetAddressList () + { + } + + /// + /// Recursively get all of the mailboxes contained within the . + /// + /// + /// This API is useful for collecting a flattened list of + /// recipients for use with sending via SMTP or for encrypting via S/MIME or PGP/MIME. + /// + /// The mailboxes. + public IEnumerable Mailboxes { + get { + foreach (var address in list) { + var group = address as GroupAddress; + + if (group != null) { + foreach (var mailbox in group.Members.Mailboxes) + yield return mailbox; + } else { + yield return (MailboxAddress) address; + } + } + + yield break; + } + } + + #region IList implementation + + /// + /// Get the index of the specified address. + /// + /// + /// Finds the index of the specified address, if it exists. + /// + /// The index of the specified address if found; otherwise -1. + /// The address to get the index of. + /// + /// is null. + /// + public int IndexOf (InternetAddress address) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + return list.IndexOf (address); + } + + /// + /// Insert an address at the specified index. + /// + /// + /// Inserts the address at the specified index in the list. + /// + /// The index to insert the address. + /// The address. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, InternetAddress address) + { + if (index < 0 || index > list.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (address == null) + throw new ArgumentNullException (nameof (address)); + + address.Changed += AddressChanged; + list.Insert (index, address); + OnChanged (); + } + + /// + /// Remove the address at the specified index. + /// + /// + /// Removes the address at the specified index. + /// + /// The index of the address to remove. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index >= list.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + list[index].Changed -= AddressChanged; + list.RemoveAt (index); + OnChanged (); + } + + /// + /// Get or set the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The internet address at the specified index. + /// The index of the address to get or set. + /// + /// is null. + /// + /// + /// is out of range. + /// + public InternetAddress this [int index] { + get { return list[index]; } + set { + if (index < 0 || index >= list.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (list[index] == value) + return; + + list[index].Changed -= AddressChanged; + value.Changed += AddressChanged; + list[index] = value; + OnChanged (); + } + } + + #endregion + + #region ICollection implementation + + /// + /// Get the number of addresses in the . + /// + /// + /// Indicates the number of addresses in the list. + /// + /// The number of addresses. + public int Count { + get { return list.Count; } + } + + /// + /// Get a value indicating whether the is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add an address to the . + /// + /// + /// Adds the specified address to the end of the address list. + /// + /// The address. + /// + /// is null. + /// + public void Add (InternetAddress address) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + address.Changed += AddressChanged; + list.Add (address); + OnChanged (); + } + + /// + /// Add a collection of addresses to the . + /// + /// + /// Adds a range of addresses to the end of the address list. + /// + /// A colelction of addresses. + /// + /// is null. + /// + public void AddRange (IEnumerable addresses) + { + if (addresses == null) + throw new ArgumentNullException (nameof (addresses)); + + bool changed = false; + + foreach (var address in addresses) { + address.Changed += AddressChanged; + list.Add (address); + changed = true; + } + + if (changed) + OnChanged (); + } + + /// + /// Clear the address list. + /// + /// + /// Removes all of the addresses from the list. + /// + public void Clear () + { + if (list.Count == 0) + return; + + for (int i = 0; i < list.Count; i++) + list[i].Changed -= AddressChanged; + + list.Clear (); + OnChanged (); + } + + /// + /// Check if the contains the specified address. + /// + /// + /// Determines whether or not the address list contains the specified address. + /// + /// true if the specified address exists; + /// otherwise false. + /// The address. + /// + /// is null. + /// + public bool Contains (InternetAddress address) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + return list.Contains (address); + } + + /// + /// Copy all of the addresses in the to the specified array. + /// + /// + /// Copies all of the addresses within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the addresses to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (InternetAddress[] array, int arrayIndex) + { + list.CopyTo (array, arrayIndex); + } + + /// + /// Remove the specified address from the . + /// + /// + /// Removes the specified address. + /// + /// true if the address was removed; otherwise false. + /// The address. + /// + /// is null. + /// + public bool Remove (InternetAddress address) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + if (list.Remove (address)) { + address.Changed -= AddressChanged; + OnChanged (); + return true; + } + + return false; + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of addresses. + /// + /// + /// Gets an enumerator for the list of addresses. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return list.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of addresses. + /// + /// + /// Gets an enumerator for the list of addresses. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return list.GetEnumerator (); + } + + #endregion + + #region IEquatable implementation + + /// + /// Determine whether the specified is equal to the current . + /// + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public bool Equals (InternetAddressList other) + { + if (other == null) + return false; + + if (other.Count != Count) + return false; + + for (int i = 0; i < Count; i++) { + if (!this[i].Equals (other[i])) + return false; + } + + return true; + } + + #endregion + + #region IComparable implementation + + /// + /// Compare two internet address lists. + /// + /// + /// Compares two internet address lists for the purpose of sorting. + /// + /// The sort order of the current internet address list compared to the other internet address list. + /// The internet address list to compare to. + /// + /// is null. + /// + public int CompareTo (InternetAddressList other) + { + int rv; + + if (other == null) + throw new ArgumentNullException (nameof (other)); + + for (int i = 0; i < Math.Min (Count, other.Count); i++) { + if ((rv = this[i].CompareTo (other[i])) != 0) + return rv; + } + + return Count - other.Count; + } + + #endregion + + /// + /// Determine whether the specified object is equal to the current object. + /// + /// + /// The type of comparison between the current instance and the parameter depends on whether + /// the current instance is a reference type or a value type. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + public override bool Equals (object obj) + { + return Equals (obj as InternetAddressList); + } + + /// + /// Return the hash code for this instance. + /// + /// + /// Returns the hash code for this instance. + /// + /// A hash code for the current object. + public override int GetHashCode () + { + return ToString ().GetHashCode (); + } + + internal void Encode (FormatOptions options, StringBuilder builder, bool firstToken, ref int lineLength) + { + for (int i = 0; i < list.Count; i++) { + if (i > 0) { + builder.Append (", "); + lineLength += 2; + } + + list[i].Encode (options, builder, firstToken && i == 0, ref lineLength); + } + } + + /// + /// Serialize an to a string, optionally encoding the list of addresses for transport. + /// + /// + /// If is true, each address in the list will be encoded + /// according to the rules defined in rfc2047. + /// If there are multiple addresses in the list, they will be separated by a comma. + /// + /// A string representing the . + /// The formatting options. + /// If set to true, each in the list will be encoded. + public string ToString (FormatOptions options, bool encode) + { + var builder = new StringBuilder (); + + if (encode) { + int lineLength = 0; + + Encode (options, builder, true, ref lineLength); + + return builder.ToString (); + } + + for (int i = 0; i < list.Count; i++) { + if (i > 0) + builder.Append (", "); + + builder.Append (list[i].ToString (options, false)); + } + + return builder.ToString (); + } + + /// + /// Serialize an to a string, optionally encoding the list of addresses for transport. + /// + /// + /// If is true, each address in the list will be encoded + /// according to the rules defined in rfc2047. + /// If there are multiple addresses in the list, they will be separated by a comma. + /// + /// A string representing the . + /// If set to true, each in the list will be encoded. + public string ToString (bool encode) + { + return ToString (FormatOptions.Default, encode); + } + + /// + /// Serialize an to a string suitable for display. + /// + /// + /// If there are multiple addresses in the list, they will be separated by a comma. + /// + /// A string representing the . + public override string ToString () + { + return ToString (FormatOptions.Default, false); + } + + internal event EventHandler Changed; + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + void AddressChanged (object sender, EventArgs e) + { + OnChanged (); + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool isGroup, int groupDepth, bool throwOnError, out List addresses) + { + var flags = throwOnError ? InternetAddress.AddressParserFlags.Parse : InternetAddress.AddressParserFlags.TryParse; + var list = new List (); + InternetAddress address; + + addresses = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index == endIndex) { + if (throwOnError) + throw new ParseException ("No addresses found.", index, index); + + return false; + } + + while (index < endIndex) { + if (isGroup && text[index] == (byte) ';') + break; + + if (!InternetAddress.TryParse (options, text, ref index, endIndex, groupDepth, flags, out address)) { + // skip this address... + while (index < endIndex && text[index] != (byte) ',' && (!isGroup || text[index] != (byte) ';')) + index++; + } else { + list.Add (address); + } + + // Note: we loop here in case there are any null addresses between commas + do { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex || text[index] != (byte) ',') + break; + + index++; + } while (true); + } + + addresses = list; + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed addresses. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out InternetAddressList addresses) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + List addrlist; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, startIndex + length, false, 0, false, out addrlist)) { + addresses = null; + return false; + } + + addresses = new InternetAddressList (addrlist); + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed addresses. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out InternetAddressList addresses) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out addresses); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the specified index. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed addresses. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out InternetAddressList addresses) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + List addrlist; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, buffer.Length, false, 0, false, out addrlist)) { + addresses = null; + return false; + } + + addresses = new InternetAddressList (addrlist); + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the specified index. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed addresses. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out InternetAddressList addresses) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out addresses); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the specified buffer. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The parsed addresses. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out InternetAddressList addresses) + { + ParseUtils.ValidateArguments (options, buffer); + + List addrlist; + int index = 0; + + if (!TryParse (options, buffer, ref index, buffer.Length, false, 0, false, out addrlist)) { + addresses = null; + return false; + } + + addresses = new InternetAddressList (addrlist); + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the specified buffer. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The input buffer. + /// The parsed addresses. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out InternetAddressList addresses) + { + return TryParse (ParserOptions.Default, buffer, out addresses); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a list of addresses from the specified text. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The parser options to use. + /// The text. + /// The parsed addresses. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out InternetAddressList addresses) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + List addrlist; + int index = 0; + + if (!TryParse (options, buffer, ref index, buffer.Length, false, 0, false, out addrlist)) { + addresses = null; + return false; + } + + addresses = new InternetAddressList (addrlist); + + return true; + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a list of addresses from the specified text. + /// + /// true, if the address list was successfully parsed, false otherwise. + /// The text. + /// The parsed addresses. + /// + /// is null. + /// + public static bool TryParse (string text, out InternetAddressList addresses) + { + return TryParse (ParserOptions.Default, text, out addresses); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + List addrlist; + int index = startIndex; + + TryParse (options, buffer, ref index, startIndex + length, false, 0, true, out addrlist); + + return new InternetAddressList (addrlist); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the given index + /// and spanning across the specified number of bytes. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + List addrlist; + int index = startIndex; + + TryParse (options, buffer, ref index, buffer.Length, false, 0, true, out addrlist); + + return new InternetAddressList (addrlist); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the supplied buffer starting at the specified index. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the specified buffer. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + List addrlist; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, false, 0, true, out addrlist); + + return new InternetAddressList (addrlist); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a list of addresses from the specified buffer. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a list of addresses from the specified text. + /// + /// The parsed . + /// The parser options to use. + /// The text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (ParserOptions options, string text) + { + ParseUtils.ValidateArguments (options, text); + + var buffer = Encoding.UTF8.GetBytes (text); + List addrlist; + int index = 0; + + TryParse (options, buffer, ref index, buffer.Length, false, 0, true, out addrlist); + + return new InternetAddressList (addrlist); + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a list of addresses from the specified text. + /// + /// The parsed . + /// The text. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static InternetAddressList Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + +#if ENABLE_SNM + /// + /// Explicit cast to convert a to a + /// . + /// + /// + /// Casts a to a + /// in cases where you might want to make use of the System.Net.Mail APIs. + /// + /// The equivalent . + /// The addresses. + /// + /// contains one or more group addresses and cannot be converted. + /// + public static explicit operator MailAddressCollection (InternetAddressList addresses) + { + if (addresses == null) + return null; + + var collection = new MailAddressCollection (); + for (int i = 0; i < addresses.Count; i++) { + if (addresses[i] is GroupAddress) + throw new InvalidCastException ("Cannot cast a MailKit.GroupAddress to a System.Net.Mail.MailAddress."); + + var mailbox = (MailboxAddress) addresses[i]; + + collection.Add ((MailAddress) mailbox); + } + + return collection; + } + + /// + /// Explicit cast to convert a + /// to a . + /// + /// + /// Casts a to a + /// in cases where you might want to make use of the the superior MimeKit APIs. + /// + /// The equivalent . + /// The mail address. + public static explicit operator InternetAddressList (MailAddressCollection addresses) + { + if (addresses == null) + return null; + + var list = new InternetAddressList (); + foreach (var address in addresses) + list.Add ((MailboxAddress) address); + + return list; + } +#endif + } +} diff --git a/src/MimeKit/MacInterop/CFArray.cs b/src/MimeKit/MacInterop/CFArray.cs new file mode 100644 index 0000000..9e38c75 --- /dev/null +++ b/src/MimeKit/MacInterop/CFArray.cs @@ -0,0 +1,56 @@ +// +// CFArray.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + class CFArray : CFObject + { + [DllImport (CoreFoundationLibrary)] + extern static IntPtr CFArrayGetValueAtIndex (IntPtr handle, IntPtr index); + + [DllImport (CoreFoundationLibrary)] + extern static int CFArrayGetCount (IntPtr handle); + + public CFArray (IntPtr handle, bool owns) : base (handle, owns) + { + } + + public CFArray (IntPtr handle) : base (handle, false) + { + } + + public int Count { + get { return CFArrayGetCount (Handle); } + } + + public IntPtr GetValue (int index) + { + return CFArrayGetValueAtIndex (Handle, new IntPtr (index)); + } + } +} diff --git a/src/MimeKit/MacInterop/CFData.cs b/src/MimeKit/MacInterop/CFData.cs new file mode 100644 index 0000000..eda7499 --- /dev/null +++ b/src/MimeKit/MacInterop/CFData.cs @@ -0,0 +1,85 @@ +// +// CFData.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + class CFData : CFObject + { + byte[] cached; + + [DllImport (CoreFoundationLibrary)] + extern static int CFDataGetLength (IntPtr handle); + + [DllImport (CoreFoundationLibrary)] + extern static void CFDataGetBytes (IntPtr handle, CFRange range, IntPtr buffer); + + [DllImport (CoreFoundationLibrary)] + extern static IntPtr CFDataCreate (IntPtr allocator, byte[] buffer, int length); + + static byte[] CFDataGetBytes (IntPtr handle) + { + if (handle == IntPtr.Zero) + return null; + + int length = CFDataGetLength (handle); + if (length < 1) + return null; + + var buffer = new byte[length]; + unsafe { + fixed (byte *bufptr = buffer) { + CFDataGetBytes (handle, new CFRange (0, length), (IntPtr) bufptr); + } + } + + return buffer; + } + + public CFData (IntPtr handle, bool owns) : base (handle, owns) + { + } + + public CFData (IntPtr handle) : base (handle, false) + { + } + + public CFData (byte[] buffer) + { + Handle = CFDataCreate (IntPtr.Zero, buffer, buffer.Length); + cached = buffer; + } + + public byte[] GetBuffer () + { + if (cached == null) + cached = CFDataGetBytes (Handle); + + return cached; + } + } +} diff --git a/src/MimeKit/MacInterop/CFDictionary.cs b/src/MimeKit/MacInterop/CFDictionary.cs new file mode 100644 index 0000000..937a355 --- /dev/null +++ b/src/MimeKit/MacInterop/CFDictionary.cs @@ -0,0 +1,177 @@ +// +// CFDictionary.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + class CFDictionary : CFObject + { + public static IntPtr KeyCallbacks; + public static IntPtr ValueCallbacks; + + static CFDictionary () + { + var lib = Dlfcn.dlopen (CoreFoundationLibrary, 0); + + try { + KeyCallbacks = Dlfcn.GetIndirect (lib, "kCFTypeDictionaryKeyCallBacks"); + ValueCallbacks = Dlfcn.GetIndirect (lib, "kCFTypeDictionaryValueCallBacks"); + } finally { + Dlfcn.dlclose (lib); + } + } + + public CFDictionary (IntPtr handle, bool owns) : base (handle, owns) + { + } + + public CFDictionary (IntPtr handle) : base (handle, false) + { + } + + public static CFDictionary FromObjectAndKey (CFObject obj, CFObject key) + { + return new CFDictionary (CFDictionaryCreate (IntPtr.Zero, new IntPtr[] { key.Handle }, new IntPtr [] { obj.Handle }, 1, KeyCallbacks, ValueCallbacks), true); + } + + public static CFDictionary FromObjectsAndKeys (CFObject[] objects, CFObject[] keys) + { + if (objects == null) + throw new ArgumentNullException ("objects"); + + if (keys == null) + throw new ArgumentNullException ("keys"); + + if (objects.Length != keys.Length) + throw new ArgumentException ("The length of both arrays must be the same"); + + IntPtr [] k = new IntPtr [keys.Length]; + IntPtr [] v = new IntPtr [keys.Length]; + + for (int i = 0; i < k.Length; i++) { + k [i] = keys [i].Handle; + v [i] = objects [i].Handle; + } + + return new CFDictionary (CFDictionaryCreate (IntPtr.Zero, k, v, k.Length, KeyCallbacks, ValueCallbacks), true); + } + + [DllImport (CoreFoundationLibrary)] + extern static IntPtr CFDictionaryCreate (IntPtr allocator, IntPtr[] keys, IntPtr[] vals, int len, IntPtr keyCallbacks, IntPtr valCallbacks); + + [DllImport (CoreFoundationLibrary)] + extern static IntPtr CFDictionaryGetValue (IntPtr theDict, IntPtr key); + public static IntPtr GetValue (IntPtr theDict, IntPtr key) + { + return CFDictionaryGetValue (theDict, key); + } + +// public static bool GetBooleanValue (IntPtr theDict, IntPtr key) +// { +// var value = GetValue (theDict, key); +// if (value == IntPtr.Zero) +// return false; +// return CFBoolean.GetValue (value); +// } + + public string GetStringValue (string key) + { + using (var str = new CFString (key)) { + using (var value = new CFString (CFDictionaryGetValue (Handle, str.Handle))) { + return value.ToString (); + } + } + } + + public int GetInt32Value (string key) + { + int value = 0; + using (var str = new CFString (key)) { + if (!CFNumberGetValue (CFDictionaryGetValue (Handle, str.Handle), /* kCFNumberSInt32Type */ 3, out value)) + throw new System.Collections.Generic.KeyNotFoundException (string.Format ("Key {0} not found", key)); + return value; + } + } + + public IntPtr GetIntPtrValue (string key) + { + using (var str = new CFString (key)) { + return CFDictionaryGetValue (Handle, str.Handle); + } + } + + public CFDictionary GetDictionaryValue (string key) + { + using (var str = new CFString (key)) { + var ptr = CFDictionaryGetValue (Handle, str.Handle); + return ptr == IntPtr.Zero ? null : new CFDictionary (ptr); + } + } + + public bool ContainsKey (string key) + { + using (var str = new CFString (key)) { + return CFDictionaryContainsKey (Handle, str.Handle); + } + } + + [DllImport (CoreFoundationLibrary)] + static extern bool CFNumberGetValue (IntPtr number, int theType, out int value); + + [DllImport (CoreFoundationLibrary)] + extern static bool CFDictionaryContainsKey (IntPtr theDict, IntPtr key); + } + + class CFMutableDictionary : CFDictionary + { + [DllImport (CoreFoundationLibrary)] + static extern IntPtr CFDictionaryCreateMutable (IntPtr allocator, IntPtr capacity, IntPtr keyCallBacks, IntPtr valueCallBacks); + + // void CFDictionaryAddValue (CFMutableDictionaryRef theDict, const void *key, const void *value); + + [DllImport (CoreFoundationLibrary)] + extern static void CFDictionarySetValue (IntPtr theDict, IntPtr key, IntPtr value); + + public CFMutableDictionary (IntPtr handle, bool owns) : base (handle, owns) + { + } + + public CFMutableDictionary (IntPtr handle) : base (handle, false) + { + } + + public void SetValue (IntPtr key, IntPtr value) + { + CFDictionarySetValue (Handle, key, value); + } + +// public void SetValue (IntPtr key, bool value) +// { +// SetValue (key, value ? CFBoolean.True.Handle : CFBoolean.False.Handle); +// } + } +} diff --git a/src/MimeKit/MacInterop/CFObject.cs b/src/MimeKit/MacInterop/CFObject.cs new file mode 100644 index 0000000..00d4173 --- /dev/null +++ b/src/MimeKit/MacInterop/CFObject.cs @@ -0,0 +1,76 @@ +// +// CFObject.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + abstract class CFObject : IDisposable + { + protected const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + internal extern static IntPtr CFRelease (IntPtr handle); + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + internal extern static IntPtr CFRetain (IntPtr handle); + + public IntPtr Handle { + get; protected set; + } + + protected CFObject (IntPtr handle, bool owns) + { + if (!owns) + CFRetain (handle); + + Handle = handle; + } + + protected CFObject () + { + } + + ~CFObject () + { + Dispose (false); + } + + protected virtual void Dispose (bool disposing) + { + if (Handle != IntPtr.Zero){ + CFRelease (Handle); + Handle = IntPtr.Zero; + } + } + + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + } +} diff --git a/src/MimeKit/MacInterop/CFRange.cs b/src/MimeKit/MacInterop/CFRange.cs new file mode 100644 index 0000000..8519c15 --- /dev/null +++ b/src/MimeKit/MacInterop/CFRange.cs @@ -0,0 +1,55 @@ +// +// CFRange.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + [StructLayout (LayoutKind.Sequential)] + struct CFRange { + readonly IntPtr location; + readonly IntPtr length; + + public int Location { + get { return location.ToInt32 (); } + } + + public int Length { + get { return length.ToInt32 (); } + } + + public CFRange (int location, int length) + : this ((long) location, (long) length) + { + } + + public CFRange (long location, long length) + { + this.location = new IntPtr (location); + this.length = new IntPtr (length); + } + } +} diff --git a/src/MimeKit/MacInterop/CFString.cs b/src/MimeKit/MacInterop/CFString.cs new file mode 100644 index 0000000..6c856fe --- /dev/null +++ b/src/MimeKit/MacInterop/CFString.cs @@ -0,0 +1,131 @@ +// +// CFString.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + class CFString : CFObject + { + string cached; + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + extern static IntPtr CFStringCreateWithCharacters (IntPtr allocator, string str, int count); + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + extern static int CFStringGetLength (IntPtr handle); + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + extern static IntPtr CFStringGetCharactersPtr (IntPtr handle); + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + extern static IntPtr CFStringGetCharacters (IntPtr handle, CFRange range, IntPtr buffer); + + public CFString (string str) + { + if (str == null) + throw new ArgumentNullException ("str"); + + Handle = CFStringCreateWithCharacters (IntPtr.Zero, str, str.Length); + this.cached = str; + } + + public CFString (IntPtr handle, bool owns) : base (handle, owns) + { + } + + public CFString (IntPtr handle) : base (handle, false) + { + } + + static string CFStringGetString (IntPtr handle) + { + if (handle == IntPtr.Zero) + return null; + + string str; + + int length = CFStringGetLength (handle); + IntPtr unicode = CFStringGetCharactersPtr (handle); + IntPtr buffer = IntPtr.Zero; + + if (unicode == IntPtr.Zero) { + CFRange range = new CFRange (0, length); + buffer = Marshal.AllocCoTaskMem (length * 2); + CFStringGetCharacters (handle, range, buffer); + unicode = buffer; + } + + unsafe { + str = new string ((char *) unicode, 0, length); + } + + if (buffer != IntPtr.Zero) + Marshal.FreeCoTaskMem (buffer); + + return str; + } + + public static implicit operator string (CFString str) + { + return str.ToString (); + } + + public static implicit operator CFString (string str) + { + return new CFString (str); + } + + public int Length { + get { + if (cached != null) + return cached.Length; + + return CFStringGetLength (Handle); + } + } + + [DllImport (CoreFoundationLibrary, CharSet=CharSet.Unicode)] + extern static char CFStringGetCharacterAtIndex (IntPtr handle, int p); + + public char this [int index] { + get { + if (cached != null) + return cached[index]; + + return CFStringGetCharacterAtIndex (Handle, index); + } + } + + public override string ToString () + { + if (cached == null) + cached = CFStringGetString (Handle); + + return cached; + } + } +} diff --git a/src/MimeKit/MacInterop/CssmDbAttributeFormat.cs b/src/MimeKit/MacInterop/CssmDbAttributeFormat.cs new file mode 100644 index 0000000..21d71e9 --- /dev/null +++ b/src/MimeKit/MacInterop/CssmDbAttributeFormat.cs @@ -0,0 +1,41 @@ +// +// CssmDbAttributeFormat.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum CssmDbAttributeFormat : int { + String = 0, + Int32 = 1, + UInt32 = 2, + BigNum = 3, + Real = 4, + DateTime = 5, + Blob = 6, + MultiUInt32 = 7, + Complex = 8 + } +} diff --git a/src/MimeKit/MacInterop/CssmKeyUse.cs b/src/MimeKit/MacInterop/CssmKeyUse.cs new file mode 100644 index 0000000..ced4315 --- /dev/null +++ b/src/MimeKit/MacInterop/CssmKeyUse.cs @@ -0,0 +1,43 @@ +// +// CssmKeyUse.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + [Flags] + enum CssmKeyUse : uint { + Any = 0x80000000, + Encrypt = 0x00000001, + Decrypt = 0x00000002, + Sign = 0x00000004, + Verify = 0x00000008, + SignRecover = 0x00000010, + VerifyRecover = 0x00000020, + Wrap = 0x00000040, + Unwrap = 0x00000080, + Derive = 0x00000100 + } +} diff --git a/src/MimeKit/MacInterop/CssmTPAppleCertStatus.cs b/src/MimeKit/MacInterop/CssmTPAppleCertStatus.cs new file mode 100644 index 0000000..3b09f36 --- /dev/null +++ b/src/MimeKit/MacInterop/CssmTPAppleCertStatus.cs @@ -0,0 +1,39 @@ +// +// CssmTPAppleCertStatus.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + [Flags] + enum CssmTPAppleCertStatus : uint { + Expired = 0x00000001, + NotValidYet = 0x00000002, + IsInInputCerts = 0x00000004, + IsInAnchors = 0x00000008, + IsRoot = 0x00000010, + IsFromNet = 0x00000020 + } +} diff --git a/src/MimeKit/MacInterop/Dlfcn.cs b/src/MimeKit/MacInterop/Dlfcn.cs new file mode 100644 index 0000000..9d9e4d7 --- /dev/null +++ b/src/MimeKit/MacInterop/Dlfcn.cs @@ -0,0 +1,228 @@ +// +// Dlfcn.cs: Support for looking up symbols in shared libraries +// +// Authors: +// Jonathan Pryor: +// Miguel de Icaza. +// +// Copyright 2009-2010, Novell, Inc. +// Copyright 2011, 2012 Xamarin Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + static class Dlfcn + { + const string SystemLibrary = "/usr/lib/libSystem.dylib"; + + [DllImport (SystemLibrary)] + public static extern int dlclose (IntPtr handle); + + [DllImport (SystemLibrary)] + public static extern IntPtr dlopen (string path, int mode); + + [DllImport (SystemLibrary)] + public static extern IntPtr dlsym (IntPtr handle, string symbol); + + [DllImport (SystemLibrary, EntryPoint = "dlerror")] + internal static extern IntPtr _dlerror (); + + public static string dlerror () + { + // we can't free the string returned from dlerror + return Marshal.PtrToStringAnsi (_dlerror ()); + } + + public static CFString GetStringConstant (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return null; + + var actual = Marshal.ReadIntPtr (indirect); + if (actual == IntPtr.Zero) + return null; + + return new CFString (actual); + } + + public static IntPtr GetIndirect (IntPtr handle, string symbol) + { + return dlsym (handle, symbol); + } + + public static int GetInt32 (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return 0; + return Marshal.ReadInt32 (indirect); + } + + public static void SetInt32 (IntPtr handle, string symbol, int value) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return; + Marshal.WriteInt32 (indirect, value); + } + + public static long GetInt64 (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return 0; + return Marshal.ReadInt64 (indirect); + } + + public static void SetInt64 (IntPtr handle, string symbol, long value) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return; + Marshal.WriteInt64 (indirect, value); + } + + public static IntPtr GetIntPtr (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return IntPtr.Zero; + return Marshal.ReadIntPtr (indirect); + } + + public static void SetIntPtr (IntPtr handle, string symbol, IntPtr value) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return; + Marshal.WriteIntPtr (indirect, value); + } + + public static double GetDouble (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return 0; + unsafe { + double *d = (double *) indirect; + + return *d; + } + } + + public static void SetDouble (IntPtr handle, string symbol, double value) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return; + unsafe { + *(double *) indirect = value; + } + } + + public static float GetFloat (IntPtr handle, string symbol) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return 0; + unsafe { + float *d = (float *) indirect; + + return *d; + } + } + + public static void SetFloat (IntPtr handle, string symbol, float value) + { + var indirect = dlsym (handle, symbol); + if (indirect == IntPtr.Zero) + return; + unsafe { + *(float *) indirect = value; + } + } + + internal static int SlowGetInt32 (string lib, string symbol) + { + var handle = dlopen (lib, 0); + if (handle == IntPtr.Zero) + return 0; + try { + return GetInt32 (handle, symbol); + } finally { + dlclose (handle); + } + } + + internal static long SlowGetInt64 (string lib, string symbol) + { + var handle = dlopen (lib, 0); + if (handle == IntPtr.Zero) + return 0; + try { + return GetInt64 (handle, symbol); + } finally { + dlclose (handle); + } + } + + internal static IntPtr SlowGetIntPtr (string lib, string symbol) + { + var handle = dlopen (lib, 0); + if (handle == IntPtr.Zero) + return IntPtr.Zero; + try { + return GetIntPtr (handle, symbol); + } finally { + dlclose (handle); + } + } + + internal static double SlowGetDouble (string lib, string symbol) + { + var handle = dlopen (lib, 0); + if (handle == IntPtr.Zero) + return 0; + try { + return GetDouble (handle, symbol); + } finally { + dlclose (handle); + } + } + + internal static CFString SlowGetStringConstant (string lib, string symbol) + { + var handle = dlopen (lib, 0); + if (handle == IntPtr.Zero) + return null; + + try { + return GetStringConstant (handle, symbol); + } finally { + dlclose (handle); + } + } + } +} diff --git a/src/MimeKit/MacInterop/OSStatus.cs b/src/MimeKit/MacInterop/OSStatus.cs new file mode 100644 index 0000000..d3369f0 --- /dev/null +++ b/src/MimeKit/MacInterop/OSStatus.cs @@ -0,0 +1,40 @@ +// +// OSStatus.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum OSStatus { + Ok = 0, + AuthFailed = -25293, + NoSuchKeychain = -25294, + DuplicateKeychain = -25296, + DuplicateItem = -25299, + ItemNotFound = -25300, + NoDefaultKeychain = -25307, + DecodeError = -26275, + } +} diff --git a/src/MimeKit/MacInterop/SecCertificate.cs b/src/MimeKit/MacInterop/SecCertificate.cs new file mode 100644 index 0000000..2cc3cc4 --- /dev/null +++ b/src/MimeKit/MacInterop/SecCertificate.cs @@ -0,0 +1,69 @@ +// +// SecCertificate.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + class SecCertificate : CFObject + { + const string SecurityLibrary = "/System/Library/Frameworks/Security.framework/Security"; + + [DllImport (SecurityLibrary)] + static extern IntPtr SecCertificateCreateWithData (IntPtr allocator, IntPtr data); + + [DllImport (SecurityLibrary)] + static extern IntPtr SecCertificateCopyData (IntPtr certificate); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecCertificateCopyCommonName (IntPtr certificate, out IntPtr commonName); + + public SecCertificate (IntPtr handle, bool own) : base (handle, own) + { + } + + public SecCertificate (IntPtr handle) : base (handle, false) + { + } + + public static SecCertificate Create (CFData data) + { + return new SecCertificate (SecCertificateCreateWithData (IntPtr.Zero, data.Handle), true); + } + + public static SecCertificate Create (byte[] rawData) + { + using (var data = new CFData (rawData)) { + return Create (data); + } + } + + public CFData GetData () + { + return new CFData (SecCertificateCopyData (Handle), true); + } + } +} diff --git a/src/MimeKit/MacInterop/SecExternalFormat.cs b/src/MimeKit/MacInterop/SecExternalFormat.cs new file mode 100644 index 0000000..9857ef6 --- /dev/null +++ b/src/MimeKit/MacInterop/SecExternalFormat.cs @@ -0,0 +1,57 @@ +// +// SecExternalFormat.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum SecExternalFormat : uint { + Unknown = 0, + + // Asymmetric Key Formats + OpenSSL, + SSH, + BSAFE, + SSHv2, + + // Symmetric Key Formats + RawKey, + + // Formats for wrapped symmetric and private keys + WrappedPKCS8, + WrappedOpenSSL, + WrappedSSH, + WrappedLSH, // not supported + + // Formats for certificates + X509Cert, + + // Aggregate Types + PEMSequence, + PKCS7, + PKCS12, + NetscapeCertSequence + } +} diff --git a/src/MimeKit/MacInterop/SecItemAttr.cs b/src/MimeKit/MacInterop/SecItemAttr.cs new file mode 100644 index 0000000..e1f1538 --- /dev/null +++ b/src/MimeKit/MacInterop/SecItemAttr.cs @@ -0,0 +1,60 @@ +// +// SecItemAttr.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum SecItemAttr : int { + CreationDate = 1667522932, + ModDate = 1835295092, + Description = 1684370275, + Comment = 1768123764, + Creator = 1668445298, + Type = 1954115685, + ScriptCode = 1935897200, + Label = 1818321516, + Invisible = 1768846953, + Negative = 1852139361, + CustomIcon = 1668641641, + Account = 1633903476, + Service = 1937138533, + Generic = 1734700641, + SecurityDomain = 1935961454, + Server = 1936881266, + AuthType = 1635023216, + Port = 1886351988, + Path = 1885434984, + Volume = 1986817381, + Address = 1633969266, + Signature = 1936943463, + Protocol = 1886675820, + CertificateType = 1668577648, + CertificateEncoding = 1667591779, + CrlType = 1668445296, + CrlEncoding = 1668443747, + Alias = 1634494835, + } +} diff --git a/src/MimeKit/MacInterop/SecItemClass.cs b/src/MimeKit/MacInterop/SecItemClass.cs new file mode 100644 index 0000000..cf6ddc9 --- /dev/null +++ b/src/MimeKit/MacInterop/SecItemClass.cs @@ -0,0 +1,40 @@ +// +// SecItemClass.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum SecItemClass : uint + { + InternetPassword = 1768842612, // 'inet' + GenericPassword = 1734700656, // 'genp' + AppleSharePassword = 1634953328, // 'ashp' + Certificate = 0x80000000 + 0x1000, + PublicKey = 0x0000000A + 5, + PrivateKey = 0x0000000A + 6, + SymmetricKey = 0x0000000A + 7 + } +} diff --git a/src/MimeKit/MacInterop/SecItemExportFlags.cs b/src/MimeKit/MacInterop/SecItemExportFlags.cs new file mode 100644 index 0000000..feecf84 --- /dev/null +++ b/src/MimeKit/MacInterop/SecItemExportFlags.cs @@ -0,0 +1,34 @@ +// +// SecItemExportFlags.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum SecItemImportExportFlags : uint { + None = 0x00000000, + PemArmour = 0x00000001, + } +} diff --git a/src/MimeKit/MacInterop/SecKeyAttribute.cs b/src/MimeKit/MacInterop/SecKeyAttribute.cs new file mode 100644 index 0000000..789e854 --- /dev/null +++ b/src/MimeKit/MacInterop/SecKeyAttribute.cs @@ -0,0 +1,59 @@ +// +// SecKeyAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.MacInterop { + enum SecKeyAttribute { + KeyClass = 0, + PrintName = 1, + Alias = 2, + Permanent = 3, + Private = 4, + Modifiable = 5, + Label = 6, + ApplicationTag = 7, + KeyCreator = 8, + KeyType = 9, + KeySizeInBits = 10, + EffectiveKeySize = 11, + StartDate = 12, + EndDate = 13, + Sensitive = 14, + AlwaysSensitive = 15, + Extractable = 16, + NeverExtractable = 17, + Encrypt = 18, + Decrypt = 19, + Derive = 20, + Sign = 21, + Verify = 22, + SignRecover = 23, + VerifyRecover = 24, + Wrap = 25, + Unwrap = 26, + } +} diff --git a/src/MimeKit/MacInterop/SecKeychain.cs b/src/MimeKit/MacInterop/SecKeychain.cs new file mode 100644 index 0000000..89ae58f --- /dev/null +++ b/src/MimeKit/MacInterop/SecKeychain.cs @@ -0,0 +1,471 @@ +// +// SecKeychain.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Org.BouncyCastle.Security.Certificates; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Pkcs; + +using MimeKit.Cryptography; + +namespace MimeKit.MacInterop { + class SecKeychain : CFObject + { + const string SecurityLibrary = "/System/Library/Frameworks/Security.framework/Security"; + + /// + /// The default login keychain. + /// + public static readonly SecKeychain Default = GetDefault (); + + bool disposed; + + SecKeychain (IntPtr handle, bool owns) : base (handle, owns) + { + } + + SecKeychain (IntPtr handle) : base (handle, false) + { + } + + #region Managing Certificates + + [DllImport (SecurityLibrary)] + static extern OSStatus SecCertificateAddToKeychain (IntPtr certificate, IntPtr keychain); + + [DllImport (SecurityLibrary)] + static extern IntPtr SecCertificateCreateWithData (IntPtr allocator, IntPtr data); + + [DllImport (SecurityLibrary)] + static extern IntPtr SecCertificateCopyData (IntPtr certificate); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecCertificateCopyCommonName (IntPtr certificate, out IntPtr commonName); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecPKCS12Import (IntPtr pkcs12DataRef, IntPtr options, ref IntPtr items); + + #endregion + + #region Managing Identities + + [DllImport (SecurityLibrary)] + static extern OSStatus SecIdentityCopyCertificate (IntPtr identityRef, out IntPtr certificateRef); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecIdentityCopyPrivateKey (IntPtr identityRef, out IntPtr privateKeyRef); + + // WARNING: deprecated in Mac OS X 10.7 + [DllImport (SecurityLibrary)] + static extern OSStatus SecIdentitySearchCreate (IntPtr keychainOrArray, CssmKeyUse keyUsage, out IntPtr searchRef); + + // WARNING: deprecated in Mac OS X 10.7 + [DllImport (SecurityLibrary)] + static extern OSStatus SecIdentitySearchCopyNext (IntPtr searchRef, out IntPtr identity); + + // Note: SecIdentitySearch* has been replaced with SecItemCopyMatching + + //[DllImport (SecurityLib)] + //OSStatus SecItemCopyMatching (CFDictionaryRef query, CFTypeRef *result); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecItemImport (IntPtr importedData, IntPtr fileName, ref SecExternalFormat format, IntPtr type, SecItemImportExportFlags flags, IntPtr keyParams, IntPtr keychain, ref IntPtr items); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecItemExport (IntPtr itemRef, SecExternalFormat format, SecItemImportExportFlags flags, IntPtr keyParams, out IntPtr exportedData); + + #endregion + + #region Getting Information About Security Result Codes + + [DllImport (SecurityLibrary)] + static extern IntPtr SecCopyErrorMessageString (OSStatus status, IntPtr reserved); + + #endregion + + #region Managing Keychains + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainCopyDefault (ref IntPtr keychain); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainCreate (string path, uint passwordLength, byte[] password, bool promptUser, IntPtr initialAccess, ref IntPtr keychain); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainOpen (string path, ref IntPtr keychain); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainDelete (IntPtr keychain); + + static SecKeychain GetDefault () + { + IntPtr handle = IntPtr.Zero; + + if (SecKeychainCopyDefault (ref handle) == OSStatus.Ok) + return new SecKeychain (handle, true); + + return null; + } + + /// + /// Create a keychain at the specified path with the specified password. + /// + /// The path to the keychain. + /// The password for unlocking the keychain. + /// + /// was null. + /// -or- + /// was null. + /// + /// + /// An unknown error creating the keychain occurred. + /// + public static SecKeychain Create (string path, string password) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (password == null) + throw new ArgumentNullException ("password"); + + var passwd = Encoding.UTF8.GetBytes (password); + var handle = IntPtr.Zero; + + var status = SecKeychainCreate (path, (uint) passwd.Length, passwd, false, IntPtr.Zero, ref handle); + if (status != OSStatus.Ok) + throw new Exception (GetError (status)); + + return new SecKeychain (handle); + } + + /// + /// Opens the keychain at the specified path. + /// + /// The path to the keychain. + /// + /// was null. + /// + /// + /// An unknown error opening the keychain occurred. + /// + public static SecKeychain Open (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + var handle = IntPtr.Zero; + + var status = SecKeychainOpen (path, ref handle); + if (status != OSStatus.Ok) + throw new Exception (GetError (status)); + + return new SecKeychain (handle); + } + + /// + /// Deletes the specified keychain. + /// + /// Keychain. + /// + /// was null. + /// + /// + /// has been disposed. + /// + /// + /// An unknown error deleting the keychain occurred. + /// + public static void Delete (SecKeychain keychain) + { + if (keychain == null) + throw new ArgumentNullException ("keychain"); + + if (keychain.disposed) + throw new ObjectDisposedException ("SecKeychain"); + + if (keychain.Handle == IntPtr.Zero) + throw new InvalidOperationException (); + + var status = SecKeychainDelete (keychain.Handle); + if (status != OSStatus.Ok) + throw new Exception (GetError (status)); + + keychain.Dispose (); + } + + #endregion + + #region Searching for Keychain Items + + [DllImport (SecurityLibrary)] + static extern unsafe OSStatus SecKeychainSearchCreateFromAttributes (IntPtr keychainOrArray, SecItemClass itemClass, SecKeychainAttributeList *attrList, out IntPtr searchRef); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainSearchCopyNext (IntPtr searchRef, out IntPtr itemRef); + + #endregion + + #region Creating and Deleting Keychain Items + + [DllImport (SecurityLibrary)] + static extern unsafe OSStatus SecKeychainItemCreateFromContent (SecItemClass itemClass, SecKeychainAttributeList *attrList, + uint passwordLength, byte[] password, IntPtr keychain, + IntPtr initialAccess, ref IntPtr itemRef); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainItemDelete (IntPtr itemRef); + + #endregion + + #region Managing Keychain Items + + [DllImport (SecurityLibrary)] + static extern unsafe OSStatus SecKeychainItemModifyAttributesAndData (IntPtr itemRef, SecKeychainAttributeList *attrList, uint length, byte [] data); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainItemCopyContent (IntPtr itemRef, ref SecItemClass itemClass, IntPtr attrList, ref uint length, ref IntPtr data); + + [DllImport (SecurityLibrary)] + static extern OSStatus SecKeychainItemFreeContent (IntPtr attrList, IntPtr data); + + #endregion + + static string GetError (OSStatus status) + { + CFString str = null; + + try { + str = new CFString (SecCopyErrorMessageString (status, IntPtr.Zero), true); + return str.ToString (); + } catch { + return status.ToString (); + } finally { + if (str != null) + str.Dispose (); + } + } + + /// + /// Gets a list of all certificates suitable for the given key usage. + /// + /// The matching certificates. + /// The key usage. + /// + /// The keychain has been disposed. + /// + public IList GetCertificates (CssmKeyUse keyUsage) + { + if (disposed) + throw new ObjectDisposedException ("SecKeychain"); + + var parser = new X509CertificateParser (); + var certs = new List (); + IntPtr searchRef, itemRef, certRef; + OSStatus status; + + status = SecIdentitySearchCreate (Handle, keyUsage, out searchRef); + if (status != OSStatus.Ok) + return certs; + + while (SecIdentitySearchCopyNext (searchRef, out itemRef) == OSStatus.Ok) { + if (SecIdentityCopyCertificate (itemRef, out certRef) == OSStatus.Ok) { + using (var data = new CFData (SecCertificateCopyData (certRef), true)) { + var rawData = data.GetBuffer (); + + try { + certs.Add (parser.ReadCertificate (rawData)); + } catch (CertificateException ex) { + Debug.WriteLine ("Failed to parse X509 certificate from keychain: {0}", ex); + } + } + } + + CFRelease (itemRef); + } + + CFRelease (searchRef); + + return certs; + } + + public IList GetAllCmsSigners () + { + if (disposed) + throw new ObjectDisposedException ("SecKeychain"); + + var signers = new List (); + IntPtr searchRef, itemRef, dataRef; + OSStatus status; + + status = SecIdentitySearchCreate (Handle, CssmKeyUse.Sign, out searchRef); + if (status != OSStatus.Ok) + return signers; + + while (SecIdentitySearchCopyNext (searchRef, out itemRef) == OSStatus.Ok) { + if (SecItemExport (itemRef, SecExternalFormat.PKCS12, SecItemImportExportFlags.None, IntPtr.Zero, out dataRef) == OSStatus.Ok) { + var data = new CFData (dataRef, true); + var rawData = data.GetBuffer (); + data.Dispose (); + + try { + using (var memory = new MemoryStream (rawData, false)) { + var pkcs12 = new Pkcs12Store (memory, new char[0]); + + foreach (string alias in pkcs12.Aliases) { + if (!pkcs12.IsKeyEntry (alias)) + continue; + + var chain = pkcs12.GetCertificateChain (alias); + var entry = pkcs12.GetKey (alias); + + signers.Add (new CmsSigner (chain, entry.Key)); + } + } + } catch (Exception ex) { + Debug.WriteLine ("Failed to decode keychain pkcs12 data: {0}", ex); + } + } + + CFRelease (itemRef); + } + + CFRelease (searchRef); + + return signers; + } + + public bool Add (AsymmetricKeyParameter key) + { + // FIXME: how do we convert an AsymmetricKeyParameter into something usable by MacOS? + throw new NotImplementedException (); + } + + public bool Add (X509Certificate certificate) + { + using (var cert = SecCertificate.Create (certificate.GetEncoded ())) { + var status = SecCertificateAddToKeychain (cert.Handle, Handle); + return status == OSStatus.Ok || status == OSStatus.DuplicateItem; + } + } + + public unsafe bool Contains (X509Certificate certificate) + { + if (certificate == null) + throw new ArgumentNullException ("certificate"); + + if (disposed) + throw new ObjectDisposedException ("SecKeychain"); + + // Note: we don't have to use an alias attribute, it's just that it might be faster to use it (fewer certificates we have to compare raw data for) + byte[] alias = Encoding.UTF8.GetBytes (certificate.GetCommonName ()); + IntPtr searchRef, itemRef; + bool found = false; + byte[] certData; + OSStatus status; + + fixed (byte* aliasPtr = alias) { + SecKeychainAttribute* attrs = stackalloc SecKeychainAttribute [1]; + int n = 0; + + if (alias != null) + attrs[n++] = new SecKeychainAttribute (SecItemAttr.Alias, (uint) alias.Length, (IntPtr) aliasPtr); + + SecKeychainAttributeList attrList = new SecKeychainAttributeList (n, (IntPtr) attrs); + + status = SecKeychainSearchCreateFromAttributes (Handle, SecItemClass.Certificate, &attrList, out searchRef); + if (status != OSStatus.Ok) + throw new Exception ("Could not enumerate certificates from the keychain. Error:\n" + GetError (status)); + + certData = certificate.GetEncoded (); + + while (!found && SecKeychainSearchCopyNext (searchRef, out itemRef) == OSStatus.Ok) { + SecItemClass itemClass = 0; + IntPtr data = IntPtr.Zero; + uint length = 0; + + status = SecKeychainItemCopyContent (itemRef, ref itemClass, IntPtr.Zero, ref length, ref data); + if (status == OSStatus.Ok) { + if (certData.Length == (int) length) { + byte[] rawData = new byte[(int) length]; + + Marshal.Copy (data, rawData, 0, (int) length); + + found = true; + for (int i = 0; i < rawData.Length; i++) { + if (rawData[i] != certData[i]) { + found = false; + break; + } + } + } + + SecKeychainItemFreeContent (IntPtr.Zero, data); + } + + CFRelease (itemRef); + } + + CFRelease (searchRef); + } + + return found; + } + +// public void ImportPkcs12 (byte[] rawData, string password) +// { +// if (rawData == null) +// throw new ArgumentNullException ("rawData"); +// +// if (password == null) +// throw new ArgumentNullException ("password"); +// +// if (disposed) +// throw new ObjectDisposedException ("SecKeychain"); +// +// using (var data = new CFData (rawData)) { +// var options = IntPtr.Zero; +// var items = IntPtr.Zero; +// +// var status = SecPKCS12Import (data.Handle, options, ref items); +// CFRelease (options); +// CFRelease (items); +// } +// } + + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + disposed = true; + } + } +} diff --git a/src/MimeKit/MacInterop/SecKeychainAttribute.cs b/src/MimeKit/MacInterop/SecKeychainAttribute.cs new file mode 100644 index 0000000..b25a8d6 --- /dev/null +++ b/src/MimeKit/MacInterop/SecKeychainAttribute.cs @@ -0,0 +1,45 @@ +// +// SecKeychainAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + [StructLayout (LayoutKind.Sequential)] + struct SecKeychainAttribute + { + public SecItemAttr Tag; + public uint Length; + public IntPtr Data; + + public SecKeychainAttribute (SecItemAttr tag, uint length, IntPtr data) + { + Length = length; + Data = data; + Tag = tag; + } + } +} diff --git a/src/MimeKit/MacInterop/SecKeychainAttributeList.cs b/src/MimeKit/MacInterop/SecKeychainAttributeList.cs new file mode 100644 index 0000000..8b6b6c0 --- /dev/null +++ b/src/MimeKit/MacInterop/SecKeychainAttributeList.cs @@ -0,0 +1,43 @@ +// +// SecKeychainAttributeList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2017 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.InteropServices; + +namespace MimeKit.MacInterop { + [StructLayout (LayoutKind.Sequential)] + struct SecKeychainAttributeList + { + public int Count; + public IntPtr Attrs; + + public SecKeychainAttributeList (int count, IntPtr attrs) + { + Count = count; + Attrs = attrs; + } + } +} diff --git a/src/MimeKit/MailboxAddress.cs b/src/MimeKit/MailboxAddress.cs new file mode 100644 index 0000000..f8ab8d3 --- /dev/null +++ b/src/MimeKit/MailboxAddress.cs @@ -0,0 +1,1098 @@ +// +// MailboxAddress.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections.Generic; + +#if ENABLE_SNM +using System.Net.Mail; +#endif + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A mailbox address, as specified by rfc822. + /// + /// + /// Represents a mailbox address (commonly referred to as an email address) + /// for a single recipient. + /// + public class MailboxAddress : InternetAddress + { + string address; + int at; + + internal MailboxAddress (Encoding encoding, string name, IEnumerable route, string address, int at) : base (encoding, name) + { + Route = new DomainList (route); + Route.Changed += RouteChanged; + + this.address = address; + this.at = at; + } + + internal MailboxAddress (Encoding encoding, string name, string address, int at) : base (encoding, name) + { + Route = new DomainList (); + Route.Changed += RouteChanged; + + this.address = address; + this.at = at; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name, address and route. The + /// specified text encoding is used when encoding the name according to the rules of rfc2047. + /// + /// The character encoding to be used for encoding the name. + /// The name of the mailbox. + /// The route of the mailbox. + /// The address of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is malformed. + /// + public MailboxAddress (Encoding encoding, string name, IEnumerable route, string address) : base (encoding, name) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + Route = new DomainList (route); + Route.Changed += RouteChanged; + Address = address; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name, address and route. + /// + /// The name of the mailbox. + /// The route of the mailbox. + /// The address of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is malformed. + /// + public MailboxAddress (string name, IEnumerable route, string address) : this (Encoding.UTF8, name, route, address) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified address and route. + /// + /// The route of the mailbox. + /// The address of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is malformed. + /// + [Obsolete ("This constructor will be going away. Use new MailboxAddress(string name, IEnumerable route, string address) instead.")] + public MailboxAddress (IEnumerable route, string address) : this (Encoding.UTF8, null, route, address) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name and address. The + /// specified text encoding is used when encoding the name according to the rules of rfc2047. + /// + /// The character encoding to be used for encoding the name. + /// The name of the mailbox. + /// The address of the mailbox. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is malformed. + /// + public MailboxAddress (Encoding encoding, string name, string address) : base (encoding, name) + { + if (address == null) + throw new ArgumentNullException (nameof (address)); + + Route = new DomainList (); + Route.Changed += RouteChanged; + Address = address; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified name and address. + /// + /// The name of the mailbox. + /// The address of the mailbox. + /// + /// is null. + /// + /// + /// is malformed. + /// + public MailboxAddress (string name, string address) : this (Encoding.UTF8, name, address) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified address. + /// + /// The must be in the form user@example.com. + /// This method cannot be used to parse a free-form email address that includes + /// the name or encloses the address in angle brackets. + /// To parse a free-form email address, use instead. + /// + /// + /// The address of the mailbox. + /// + /// is null. + /// + /// + /// is malformed. + /// + [Obsolete("This constructor will be going away due to it causing too much confusion. Use new MailboxAddress(string name, string address) or MailboxAddress.Parse(string) instead.")] + public MailboxAddress (string address) : this (Encoding.UTF8, null, address) + { + } + + /// + /// Clone the mailbox address. + /// + /// + /// Clones the mailbox address. + /// + /// The cloned mailbox address. + public override InternetAddress Clone () + { + return new MailboxAddress (Encoding, Name, Route, Address); + } + + /// + /// Gets the mailbox route. + /// + /// + /// A route is convention that is rarely seen in modern email systems, but is supported + /// for compatibility with email archives. + /// + /// The mailbox route. + public DomainList Route { + get; private set; + } + + /// + /// Gets or sets the mailbox address. + /// + /// + /// Represents the actual email address and is in the form of user@domain.com. + /// + /// The mailbox address. + /// + /// is null. + /// + /// + /// is malformed. + /// + public string Address { + get { return address; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (value == address) + return; + + if (value.Length > 0) { + var buffer = CharsetUtils.UTF8.GetBytes (value); + int index = 0; + + TryParseAddrspec (buffer, ref index, buffer.Length, new byte[0], true, out string addrspec, out int atIndex); + + if (index != buffer.Length) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + address = addrspec; + at = atIndex; + } else { + address = string.Empty; + at = -1; + } + + OnChanged (); + } + } + + /// + /// Gets whether or not the address is an international address. + /// + /// + /// International addresses are addresses that contain international + /// characters in either their local-parts or their domains. + /// For more information, see section 3.2 of + /// rfc6532. + /// + /// true if the address is an international address; otherwise, false. + public bool IsInternational { + get { + if (address == null) + return false; + + if (ParseUtils.IsInternational (address)) + return true; + + foreach (var domain in Route) { + if (ParseUtils.IsInternational (domain)) + return true; + } + + return false; + } + } + + static string EncodeAddrspec (string addrspec, int at) + { + if (at != -1) { + var domain = addrspec.Substring (at + 1); + var local = addrspec.Substring (0, at); + + if (ParseUtils.IsInternational (local)) + local = ParseUtils.IdnEncode (local); + + if (ParseUtils.IsInternational (domain)) + domain = ParseUtils.IdnEncode (domain); + + return local + "@" + domain; + } + + return addrspec; + } + + /// + /// Encode an addrspec token according to IDN encoding rules. + /// + /// + /// Encodes an addrspec token according to IDN encoding rules. + /// + /// The encoded addrspec token. + /// The addrspec token. + /// + /// is null. + /// + public static string EncodeAddrspec (string addrspec) + { + if (addrspec == null) + throw new ArgumentNullException (nameof (addrspec)); + + if (addrspec.Length == 0) + return addrspec; + + var buffer = CharsetUtils.UTF8.GetBytes (addrspec); + int index = 0; + + if (!TryParseAddrspec (buffer, ref index, buffer.Length, new byte[0], false, out string address, out int at)) + return addrspec; + + return EncodeAddrspec (address, at); + } + + static string DecodeAddrspec (string addrspec, int at) + { + if (at != -1) { + var domain = addrspec.Substring (at + 1); + var local = addrspec.Substring (0, at); + + if (ParseUtils.IsIdnEncoded (local)) + local = ParseUtils.IdnDecode (local); + + if (ParseUtils.IsIdnEncoded (domain)) + domain = ParseUtils.IdnDecode (domain); + + return local + "@" + domain; + } + + return addrspec; + } + + /// + /// Decode an addrspec token according to IDN decoding rules. + /// + /// + /// Decodes an addrspec token according to IDN decoding rules. + /// + /// The decoded addrspec token. + /// The addrspec token. + /// + /// is null. + /// + public static string DecodeAddrspec (string addrspec) + { + if (addrspec == null) + throw new ArgumentNullException (nameof (addrspec)); + + if (addrspec.Length == 0) + return addrspec; + + var buffer = CharsetUtils.UTF8.GetBytes (addrspec); + int index = 0; + + if (!TryParseAddrspec (buffer, ref index, buffer.Length, new byte[0], false, out string address, out int at)) + return addrspec; + + return DecodeAddrspec (address, at); + } + + /// + /// Get the mailbox address, optionally encoded according to IDN encoding rules. + /// + /// + /// If is true, then the returned mailbox address will be encoded according to the IDN encoding rules. + /// + /// true if the address should be encoded according to IDN encoding rules; otherwise, false. + /// The mailbox address. + public string GetAddress (bool idnEncode) + { + if (idnEncode) + return EncodeAddrspec (address, at); + + return DecodeAddrspec (address, at); + } + + internal override void Encode (FormatOptions options, StringBuilder builder, bool firstToken, ref int lineLength) + { + var route = Route.Encode (options); + if (!string.IsNullOrEmpty (route)) + route += ":"; + + var addrspec = GetAddress (!options.International); + + if (!string.IsNullOrEmpty (Name)) { + string name; + + if (!options.International) { + var encoded = Rfc2047.EncodePhrase (options, Encoding, Name); + name = Encoding.ASCII.GetString (encoded, 0, encoded.Length); + } else { + name = EncodeInternationalizedPhrase (Name); + } + + if (lineLength + name.Length > options.MaxLineLength) { + if (name.Length > options.MaxLineLength) { + // we need to break up the name... + builder.AppendFolded (options, firstToken, name, ref lineLength); + } else { + // the name itself is short enough to fit on a single line, + // but only if we write it on a line by itself + if (!firstToken && lineLength > 1) { + builder.LineWrap (options); + lineLength = 1; + } + + lineLength += name.Length; + builder.Append (name); + } + } else { + // we can safely fit the name on this line... + lineLength += name.Length; + builder.Append (name); + } + + if ((lineLength + route.Length + addrspec.Length + 3) > options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ("\t<"); + lineLength = 2; + } else { + builder.Append (" <"); + lineLength += 2; + } + + lineLength += route.Length; + builder.Append (route); + + lineLength += addrspec.Length + 1; + builder.Append (addrspec); + builder.Append ('>'); + } else if (!string.IsNullOrEmpty (route)) { + if (!firstToken && (lineLength + route.Length + addrspec.Length + 2) > options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ("\t<"); + lineLength = 2; + } else { + builder.Append ('<'); + lineLength++; + } + + lineLength += route.Length; + builder.Append (route); + + lineLength += addrspec.Length + 1; + builder.Append (addrspec); + builder.Append ('>'); + } else { + if (!firstToken && (lineLength + addrspec.Length) > options.MaxLineLength) { + builder.LineWrap (options); + lineLength = 1; + } + + lineLength += addrspec.Length; + builder.Append (addrspec); + } + } + + /// + /// Returns a string representation of the , + /// optionally encoding it for transport. + /// + /// + /// Returns a string containing the formatted mailbox address. If the + /// parameter is true, then the mailbox name will be encoded according to the rules defined + /// in rfc2047, otherwise the name will not be encoded at all and will therefor only be suitable + /// for display purposes. + /// + /// A string representing the . + /// The formatting options. + /// If set to true, the will be encoded. + /// + /// is null. + /// + public override string ToString (FormatOptions options, bool encode) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (encode) { + var builder = new StringBuilder (); + int lineLength = 0; + + Encode (options, builder, true, ref lineLength); + + return builder.ToString (); + } + + string route = Route.ToString (); + if (!string.IsNullOrEmpty (route)) + route += ":"; + + if (!string.IsNullOrEmpty (Name)) + return MimeUtils.Quote (Name) + " <" + route + Address + ">"; + + if (!string.IsNullOrEmpty (route)) + return "<" + route + Address + ">"; + + return Address; + } + + #region IEquatable implementation + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// Compares two mailbox addresses to determine if they are identical or not. + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals (InternetAddress other) + { + var mailbox = other as MailboxAddress; + + if (mailbox == null) + return false; + + return Name == mailbox.Name && Address == mailbox.Address; + } + + #endregion + + void RouteChanged (object sender, EventArgs e) + { + OnChanged (); + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out MailboxAddress mailbox) + { + var flags = AddressParserFlags.AllowMailboxAddress; + InternetAddress address; + + if (throwOnError) + flags |= AddressParserFlags.ThrowOnError; + + if (!TryParse (options, text, ref index, endIndex, 0, flags, out address)) { + mailbox = null; + return false; + } + + mailbox = (MailboxAddress) address; + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed mailbox address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, int length, out MailboxAddress mailbox) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, false, out mailbox)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + mailbox = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// The parsed mailbox address. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + public static bool TryParse (byte[] buffer, int startIndex, int length, out MailboxAddress mailbox) + { + return TryParse (ParserOptions.Default, buffer, startIndex, length, out mailbox); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed mailbox address. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, int startIndex, out MailboxAddress mailbox) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, false, out mailbox)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + mailbox = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The starting index of the input buffer. + /// The parsed mailbox address. + /// + /// is null. + /// + /// + /// is out of range. + /// + public static bool TryParse (byte[] buffer, int startIndex, out MailboxAddress mailbox) + { + return TryParse (ParserOptions.Default, buffer, startIndex, out mailbox); + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The input buffer. + /// The parsed mailbox address. + /// + /// is null. + /// -or- + /// is null. + /// + public static bool TryParse (ParserOptions options, byte[] buffer, out MailboxAddress mailbox) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, false, out mailbox)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + mailbox = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The input buffer. + /// The parsed mailbox address. + /// + /// is null. + /// + public static bool TryParse (byte[] buffer, out MailboxAddress mailbox) + { + return TryParse (ParserOptions.Default, buffer, out mailbox); + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The parser options to use. + /// The text. + /// The parsed mailbox address. + /// + /// is null. + /// + public static bool TryParse (ParserOptions options, string text, out MailboxAddress mailbox) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, false, out mailbox)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, false) || index != endIndex) { + mailbox = null; + return false; + } + + return true; + } + + /// + /// Try to parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// true, if the address was successfully parsed, false otherwise. + /// The text. + /// The parsed mailbox address. + /// + /// is null. + /// + public static bool TryParse (string text, out MailboxAddress mailbox) + { + return TryParse (ParserOptions.Default, text, out mailbox); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (ParserOptions options, byte[] buffer, int startIndex, int length) + { + ParseUtils.ValidateArguments (options, buffer, startIndex, length); + + int endIndex = startIndex + length; + MailboxAddress mailbox; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, true, out mailbox)) + throw new ParseException ("No mailbox address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return mailbox; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// The number of bytes in the input buffer to parse. + /// + /// is null. + /// + /// + /// and do not specify + /// a valid range in the byte array. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (byte[] buffer, int startIndex, int length) + { + return Parse (ParserOptions.Default, buffer, startIndex, length); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (ParserOptions options, byte[] buffer, int startIndex) + { + ParseUtils.ValidateArguments (options, buffer, startIndex); + + int endIndex = buffer.Length; + MailboxAddress mailbox; + int index = startIndex; + + if (!TryParse (options, buffer, ref index, endIndex, true, out mailbox)) + throw new ParseException ("No mailbox address found.", startIndex, startIndex); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return mailbox; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// The starting index of the input buffer. + /// + /// is null. + /// + /// + /// is out of range. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (byte[] buffer, int startIndex) + { + return Parse (ParserOptions.Default, buffer, startIndex); + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The input buffer. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (ParserOptions options, byte[] buffer) + { + ParseUtils.ValidateArguments (options, buffer); + + int endIndex = buffer.Length; + MailboxAddress mailbox; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, true, out mailbox)) + throw new ParseException ("No mailbox address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return mailbox; + } + + /// + /// Parse the given input buffer into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The input buffer. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (byte[] buffer) + { + return Parse (ParserOptions.Default, buffer); + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The parser options to use. + /// The text. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (ParserOptions options, string text) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (text == null) + throw new ArgumentNullException (nameof (text)); + + var buffer = Encoding.UTF8.GetBytes (text); + int endIndex = buffer.Length; + MailboxAddress mailbox; + int index = 0; + + if (!TryParse (options, buffer, ref index, endIndex, true, out mailbox)) + throw new ParseException ("No mailbox address found.", 0, 0); + + ParseUtils.SkipCommentsAndWhiteSpace (buffer, ref index, endIndex, true); + + if (index != endIndex) + throw new ParseException (string.Format ("Unexpected token at offset {0}", index), index, index); + + return mailbox; + } + + /// + /// Parse the given text into a new instance. + /// + /// + /// Parses a single . If the address is not a mailbox address or + /// there is more than a single mailbox address, then parsing will fail. + /// + /// The parsed . + /// The text. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public static new MailboxAddress Parse (string text) + { + return Parse (ParserOptions.Default, text); + } + +#if ENABLE_SNM + /// + /// Explicit cast to convert a to a + /// . + /// + /// + /// Casts a to a + /// in cases where you might want to make use of the System.Net.Mail APIs. + /// + /// The equivalent . + /// The mailbox. + public static explicit operator MailAddress (MailboxAddress mailbox) + { + return mailbox != null ? new MailAddress (mailbox.Address, mailbox.Name, mailbox.Encoding) : null; + } + + /// + /// Explicit cast to convert a + /// to a . + /// + /// + /// Casts a to a + /// in cases where you might want to make use of the the superior MimeKit APIs. + /// + /// The equivalent . + /// The mail address. + public static explicit operator MailboxAddress (MailAddress address) + { + return address != null ? new MailboxAddress (address.DisplayName, address.Address) : null; + } +#endif + } +} diff --git a/src/MimeKit/MessageDeliveryStatus.cs b/src/MimeKit/MessageDeliveryStatus.cs new file mode 100644 index 0000000..3495bdc --- /dev/null +++ b/src/MimeKit/MessageDeliveryStatus.cs @@ -0,0 +1,152 @@ +// +// MessageDeliveryStatus.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.IO; + +namespace MimeKit { + /// + /// A message delivery status MIME part. + /// + /// + /// A message delivery status MIME part is a machine readable notification denoting the + /// delivery status of a message and has a MIME-type of message/delivery-status. + /// For more information, see rfc3464. + /// + /// + /// + /// + /// + public class MessageDeliveryStatus : MimePart + { + HeaderListCollection groups; + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MessageDeliveryStatus (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public MessageDeliveryStatus () : base ("message", "delivery-status") + { + } + + /// + /// Get the groups of delivery status fields. + /// + /// + /// Gets the groups of delivery status fields. The first + /// contains the per-message fields while each remaining contains + /// fields that pertain to particular recipients of the message. + /// For more information about these fields and their values, check out + /// rfc3464. + /// Section 2.2 defines + /// the per-message fields while + /// Section 2.3 defines + /// the per-recipient fields. + /// + /// + /// + /// + /// The fields. + public HeaderListCollection StatusGroups { + get { + if (groups == null) { + if (Content == null) { + Content = new MimeContent (new MemoryBlockStream ()); + groups = new HeaderListCollection (); + } else { + groups = new HeaderListCollection (); + + using (var stream = Content.Open ()) { + var parser = new MimeParser (stream, MimeFormat.Entity); + + while (!parser.IsEndOfStream) { + var fields = parser.ParseHeaders (); + groups.Add (fields); + } + } + } + + groups.Changed += OnGroupsChanged; + } + + return groups; + } + } + + void OnGroupsChanged (object sender, EventArgs e) + { + var stream = new MemoryBlockStream (); + var options = FormatOptions.Default; + + for (int i = 0; i < groups.Count; i++) + groups[i].WriteTo (options, stream); + + stream.Position = 0; + + Content = new MimeContent (stream); + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMessageDeliveryStatus (this); + } + } +} diff --git a/src/MimeKit/MessageDispositionNotification.cs b/src/MimeKit/MessageDispositionNotification.cs new file mode 100644 index 0000000..975fc24 --- /dev/null +++ b/src/MimeKit/MessageDispositionNotification.cs @@ -0,0 +1,129 @@ +// +// MessageDispositionNotification.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.IO; + +namespace MimeKit { + /// + /// A message disposition notification MIME part. + /// + /// + /// A message disposition notification MIME part is a machine readable notification + /// denoting the disposition of a message once it has been successfully delivered + /// and has a MIME-type of message/disposition-notification. + /// + /// + public class MessageDispositionNotification : MimePart + { + HeaderList fields; + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MessageDispositionNotification (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public MessageDispositionNotification () : base ("message", "disposition-notification") + { + } + + /// + /// Get the disposition notification fields. + /// + /// + /// Gets the disposition notification fields. + /// + /// The fields. + public HeaderList Fields { + get { + if (fields == null) { + if (Content == null) { + Content = new MimeContent (new MemoryBlockStream ()); + fields = new HeaderList (); + } else { + using (var stream = Content.Open ()) { + fields = HeaderList.Load (stream); + } + } + + fields.Changed += OnFieldsChanged; + } + + return fields; + } + } + + void OnFieldsChanged (object sender, HeaderListChangedEventArgs e) + { + var stream = new MemoryBlockStream (); + var options = FormatOptions.Default; + + fields.WriteTo (options, stream); + stream.Position = 0; + + Content = new MimeContent (stream); + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMessageDispositionNotification (this); + } + } +} diff --git a/src/MimeKit/MessageIdList.cs b/src/MimeKit/MessageIdList.cs new file mode 100644 index 0000000..52c435c --- /dev/null +++ b/src/MimeKit/MessageIdList.cs @@ -0,0 +1,378 @@ +// +// MessageIdList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A list of Message-Ids. + /// + /// + /// Used by the property. + /// + public class MessageIdList : IList + { + readonly List references; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new, empty, . + /// + public MessageIdList () + { + references = new List (); + } + + /// + /// Clones the . + /// + /// + /// Creates an exact copy of the . + /// + /// An exact copy of the . + public MessageIdList Clone () + { + var clone = new MessageIdList (); + + for (int i = 0; i < references.Count; i++) + clone.references.Add (references[i]); + + return clone; + } + + #region IList implementation + + /// + /// Get the index of the requested Message-Id, if it exists. + /// + /// + /// Finds the index of the specified Message-Id, if it exists. + /// + /// The index of the requested Message-Id; otherwise -1. + /// The Message-Id. + /// + /// is null. + /// + public int IndexOf (string messageId) + { + if (messageId == null) + throw new ArgumentNullException (nameof (messageId)); + + return references.IndexOf (messageId); + } + + static string ValidateMessageId (string messageId) + { + if (messageId.Length < 2 || messageId[0] != '<' || messageId[messageId.Length - 1] != '>') + return messageId; + + return messageId.Substring (1, messageId.Length - 2); + } + + /// + /// Insert the Message-Id at the specified index. + /// + /// + /// Inserts the Message-Id at the specified index in the list. + /// + /// The index to insert the Message-Id. + /// The Message-Id to insert. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, string messageId) + { + if (messageId == null) + throw new ArgumentNullException (nameof (messageId)); + + references.Insert (index, ValidateMessageId (messageId)); + OnChanged (); + } + + /// + /// Remove the Message-Id at the specified index. + /// + /// + /// Removes the Message-Id at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + references.RemoveAt (index); + OnChanged (); + } + + /// + /// Get or set the Message-Id at the specified index. + /// + /// + /// Gets or sets the Message-Id at the specified index. + /// + /// The Message-Id at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public string this [int index] { + get { return references[index]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (references[index] == value) + return; + + references[index] = ValidateMessageId (value); + OnChanged (); + } + } + + #endregion + + #region ICollection implementation + + /// + /// Add the specified Message-Id. + /// + /// + /// Adds the specified Message-Id to the end of the list. + /// + /// The Message-Id. + /// + /// is null. + /// + public void Add (string messageId) + { + if (messageId == null) + throw new ArgumentNullException (nameof (messageId)); + + references.Add (ValidateMessageId (messageId)); + OnChanged (); + } + + /// + /// Add a collection of Message-Id items. + /// + /// + /// Adds a collection of Message-Id items to append to the list. + /// + /// The Message-Id items to add. + /// + /// is null. + /// + public void AddRange (IEnumerable items) + { + if (items == null) + throw new ArgumentNullException (nameof (items)); + + foreach (var msgid in items) + references.Add (ValidateMessageId (msgid)); + + OnChanged (); + } + + /// + /// Clear the Message-Id list. + /// + /// + /// Removes all of the Message-Ids in the list. + /// + public void Clear () + { + references.Clear (); + OnChanged (); + } + + /// + /// Check if the contains the specified Message-Id. + /// + /// + /// Determines whether or not the list contains the specified Message-Id. + /// + /// true if the specified Message-Id is contained; + /// otherwise false. + /// The Message-Id. + /// + /// is null. + /// + public bool Contains (string messageId) + { + if (messageId == null) + throw new ArgumentNullException (nameof (messageId)); + + return references.Contains (messageId); + } + + /// + /// Copy all of the Message-Ids in the to the specified array. + /// + /// + /// Copies all of the Message-Ids within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the Message-Ids to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (string[] array, int arrayIndex) + { + references.CopyTo (array, arrayIndex); + } + + /// + /// Remove a Message-Id from the . + /// + /// + /// Removes the first instance of the specified Message-Id from the list if it exists. + /// + /// true if the specified Message-Id was removed; + /// otherwise false. + /// The Message-Id. + /// + /// is null. + /// + public bool Remove (string messageId) + { + if (messageId == null) + throw new ArgumentNullException (nameof (messageId)); + + if (references.Remove (messageId)) { + OnChanged (); + return true; + } + + return false; + } + + /// + /// Get the number of Message-Ids in the . + /// + /// + /// Indicates the number of Message-Ids in the list. + /// + /// The number of Message-Ids. + public int Count { + get { return references.Count; } + } + + /// + /// Get a value indicating whether the is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of Message-Ids. + /// + /// + /// Gets an enumerator for the list of Message-Ids. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return references.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of Message-Ids. + /// + /// + /// Gets an enumerator for the list of Message-Ids. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return references.GetEnumerator (); + } + + #endregion + + /// + /// Serialize a to a string. + /// + /// + /// Each Message-Id will be surrounded by angle brackets. + /// If there are multiple Message-Ids in the list, they will be separated by whitespace. + /// + /// A string representing the . + public override string ToString () + { + var builder = new StringBuilder (); + + for (int i = 0; i < references.Count; i++) { + if (builder.Length > 0) + builder.Append (' '); + + builder.Append ('<'); + builder.Append (references[i]); + builder.Append ('>'); + } + + return builder.ToString (); + } + + internal event EventHandler Changed; + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + } +} diff --git a/src/MimeKit/MessageImportance.cs b/src/MimeKit/MessageImportance.cs new file mode 100644 index 0000000..24867ca --- /dev/null +++ b/src/MimeKit/MessageImportance.cs @@ -0,0 +1,50 @@ +// +// MessageImportance.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// An enumeration of message importance values. + /// + /// + /// Indicates the importance of a message. + /// + public enum MessageImportance { + /// + /// The message is of low importance. + /// + Low, + + /// + /// The message is of normal importance. + /// + Normal, + + /// + /// The message is of high importance. + /// + High + } +} diff --git a/src/MimeKit/MessagePart.cs b/src/MimeKit/MessagePart.cs new file mode 100644 index 0000000..763ad3f --- /dev/null +++ b/src/MimeKit/MessagePart.cs @@ -0,0 +1,288 @@ +// +// MessagePart.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using MimeKit.IO; + +namespace MimeKit { + /// + /// A MIME part containing a as its content. + /// + /// + /// Represents MIME entities such as those with a Content-Type of message/rfc822 or message/news. + /// + public class MessagePart : MimeEntity + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MessagePart (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The message subtype. + /// An array of initialization parameters: headers and message parts. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains more than one . + /// -or- + /// contains one or more arguments of an unknown type. + /// + public MessagePart (string subtype, params object[] args) : this (subtype) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + MimeMessage message = null; + + foreach (object obj in args) { + if (obj == null || TryInit (obj)) + continue; + + if (obj is MimeMessage mesg) { + if (message != null) + throw new ArgumentException ("MimeMessage should not be specified more than once."); + + message = mesg; + continue; + } + + throw new ArgumentException ("Unknown initialization parameter: " + obj.GetType ()); + } + + if (message != null) + Message = message; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes the based on the provided media type and subtype. + /// + /// The media type. + /// The media subtype. + /// + /// is null. + /// -or- + /// is null. + /// + protected MessagePart (string mediaType, string mediaSubtype) : base (mediaType, mediaSubtype) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new MIME message entity with the specified subtype. + /// + /// The message subtype. + /// + /// is null. + /// + public MessagePart (string subtype) : this ("message", subtype) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message/rfc822 MIME entity. + /// + public MessagePart () : this ("rfc822") + { + } + + /// + /// Gets or sets the message content. + /// + /// + /// Gets or sets the message content. + /// + /// The message content. + public MimeMessage Message { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMessagePart (this); + } + + /// + /// Prepare the MIME entity for transport using the specified encoding constraints. + /// + /// + /// Prepares the MIME entity for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public override void Prepare (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + if (Message != null) + Message.Prepare (constraint, maxLineLength); + } + + /// + /// Write the to the output stream. + /// + /// + /// Writes the MIME entity and its message to the output stream. + /// + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override void WriteTo (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + base.WriteTo (options, stream, contentOnly, cancellationToken); + + if (Message == null) + return; + + if (Message.MboxMarker != null && Message.MboxMarker.Length != 0) { + var cancellable = stream as ICancellableStream; + + if (cancellable != null) { + cancellable.Write (Message.MboxMarker, 0, Message.MboxMarker.Length, cancellationToken); + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + stream.Write (Message.MboxMarker, 0, Message.MboxMarker.Length); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + } + + if (options.EnsureNewLine) { + options = options.Clone (); + options.EnsureNewLine = false; + } + + Message.WriteTo (options, stream, cancellationToken); + } + + /// + /// Asynchronously write the to the output stream. + /// + /// + /// Asynchronously writes the MIME entity and its message to the output stream. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteToAsync (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + await base.WriteToAsync (options, stream, contentOnly, cancellationToken).ConfigureAwait (false); + + if (Message == null) + return; + + if (Message.MboxMarker != null && Message.MboxMarker.Length != 0) { + await stream.WriteAsync (Message.MboxMarker, 0, Message.MboxMarker.Length, cancellationToken).ConfigureAwait (false); + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + } + + if (options.EnsureNewLine) { + options = options.Clone (); + options.EnsureNewLine = false; + } + + await Message.WriteToAsync (options, stream, cancellationToken).ConfigureAwait (false); + } + } +} diff --git a/src/MimeKit/MessagePartial.cs b/src/MimeKit/MessagePartial.cs new file mode 100644 index 0000000..7598486 --- /dev/null +++ b/src/MimeKit/MessagePartial.cs @@ -0,0 +1,511 @@ +// +// MessagePartial.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A MIME part containing a partial message as its content. + /// + /// + /// The "message/partial" MIME-type is used to split large messages into + /// multiple parts, typically to work around transport systems that have size + /// limitations (for example, some SMTP servers limit have a maximum message + /// size that they will accept). + /// + public class MessagePartial : MimePart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MessagePartial (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new message/partial entity. + /// Three (3) parameters must be specified in the Content-Type header + /// of a message/partial: id, number, and total. + /// The "id" parameter is a unique identifier used to match the parts together. + /// The "number" parameter is the sequential (1-based) index of the partial message fragment. + /// The "total" parameter is the total number of pieces that make up the complete message. + /// + /// The id value shared among the partial message parts. + /// The (1-based) part number for this partial message part. + /// The total number of partial message parts. + /// + /// is null. + /// + /// + /// is less than 1. + /// -or- + /// is less than . + /// + public MessagePartial (string id, int number, int total) : base ("message", "partial") + { + if (id == null) + throw new ArgumentNullException (nameof (id)); + + if (number < 1) + throw new ArgumentOutOfRangeException (nameof (number)); + + if (total < number) + throw new ArgumentOutOfRangeException (nameof (total)); + + ContentType.Parameters.Add (new Parameter ("id", id)); + ContentType.Parameters.Add (new Parameter ("number", number.ToString ())); + ContentType.Parameters.Add (new Parameter ("total", total.ToString ())); + } + + /// + /// Gets the "id" parameter of the Content-Type header. + /// + /// + /// The "id" parameter is a unique identifier used to match the parts together. + /// + /// The identifier. + public string Id { + get { return ContentType.Parameters["id"]; } + } + + /// + /// Gets the "number" parameter of the Content-Type header. + /// + /// + /// The "number" parameter is the sequential (1-based) index of the partial message fragment. + /// + /// The part number. + public int? Number { + get { + var text = ContentType.Parameters["number"]; + int number; + + if (text == null || !int.TryParse (text, out number)) + return null; + + return number; + } + } + + /// + /// Gets the "total" parameter of the Content-Type header. + /// + /// + /// The "total" parameter is the total number of pieces that make up the complete message. + /// + /// The total number of parts. + public int? Total { + get { + var text = ContentType.Parameters["total"]; + int total; + + if (text == null || !int.TryParse (text, out total)) + return null; + + return total; + } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMessagePartial (this); + } + + static MimeMessage CloneMessage (MimeMessage message) + { + var options = message.Headers.Options; + var clone = new MimeMessage (options); + + foreach (var header in message.Headers) + clone.Headers.Add (header.Clone ()); + + return clone; + } + + /// + /// Splits the specified message into multiple messages. + /// + /// + /// Splits the specified message into multiple messages, each with a + /// message/partial body no larger than the max size specified. + /// + /// An enumeration of partial messages. + /// The message. + /// The maximum size for each message body. + /// + /// is null. + /// + /// + /// is less than 1. + /// + public static IEnumerable Split (MimeMessage message, int maxSize) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + if (maxSize < 1) + throw new ArgumentOutOfRangeException (nameof (maxSize)); + + var options = FormatOptions.CloneDefault (); + foreach (HeaderId id in Enum.GetValues (typeof (HeaderId))) { + switch (id) { + case HeaderId.Subject: + case HeaderId.MessageId: + case HeaderId.Encrypted: + case HeaderId.MimeVersion: + case HeaderId.ContentAlternative: + case HeaderId.ContentBase: + case HeaderId.ContentClass: + case HeaderId.ContentDescription: + case HeaderId.ContentDisposition: + case HeaderId.ContentDuration: + case HeaderId.ContentFeatures: + case HeaderId.ContentId: + case HeaderId.ContentIdentifier: + case HeaderId.ContentLanguage: + case HeaderId.ContentLength: + case HeaderId.ContentLocation: + case HeaderId.ContentMd5: + case HeaderId.ContentReturn: + case HeaderId.ContentTransferEncoding: + case HeaderId.ContentTranslationType: + case HeaderId.ContentType: + break; + default: + options.HiddenHeaders.Add (id); + break; + } + } + + var memory = new MemoryStream (); + + message.WriteTo (options, memory); + memory.Seek (0, SeekOrigin.Begin); + + if (memory.Length <= maxSize) { + memory.Dispose (); + + yield return message; + yield break; + } + + var streams = new List (); +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var buf = memory.GetBuffer (); +#else + var buf = memory.ToArray (); +#endif + long startIndex = 0; + + while (startIndex < memory.Length) { + // Preferably, we'd split on whole-lines if we can, + // but if that's not possible, split on max size + long endIndex = Math.Min (memory.Length, startIndex + maxSize); + + if (endIndex < memory.Length) { + long ebx = endIndex; + + while (ebx > (startIndex + 1) && buf[ebx] != (byte) '\n') + ebx--; + + if (buf[ebx] == (byte) '\n') + endIndex = ebx + 1; + } + + streams.Add (new BoundStream (memory, startIndex, endIndex, true)); + startIndex = endIndex; + } + + var msgid = message.MessageId ?? MimeUtils.GenerateMessageId (); + int number = 1; + + foreach (var stream in streams) { + var part = new MessagePartial (msgid, number++, streams.Count) { + Content = new MimeContent (stream) + }; + + var submessage = CloneMessage (message); + submessage.MessageId = MimeUtils.GenerateMessageId (); + submessage.Body = part; + + yield return submessage; + } + + yield break; + } + + static int PartialCompare (MessagePartial partial1, MessagePartial partial2) + { + if (!partial1.Number.HasValue || !partial2.Number.HasValue || partial1.Id != partial2.Id) + throw new ArgumentException ("Partial messages have mismatching identifiers.", "partials"); + + return partial1.Number.Value - partial2.Number.Value; + } + + static void CombineHeaders (MimeMessage message, MimeMessage joined) + { + var headers = new List
(); + int i = 0; + + // RFC2046: Any header fields in the enclosed message which do not start with "Content-" + // (except for the "Subject", "Message-ID", "Encrypted", and "MIME-Version" fields) will + // be ignored and dropped. + while (i < joined.Headers.Count) { + var header = joined.Headers[i]; + + switch (header.Id) { + case HeaderId.Subject: + case HeaderId.MessageId: + case HeaderId.Encrypted: + case HeaderId.MimeVersion: + headers.Add (header); + header.Offset = null; + i++; + break; + default: + joined.Headers.RemoveAt (i); + break; + } + } + + // RFC2046: All of the header fields from the initial enclosing message, except + // those that start with "Content-" and the specific header fields "Subject", + // "Message-ID", "Encrypted", and "MIME-Version", must be copied, in order, + // to the new message. + i = 0; + foreach (var header in message.Headers) { + switch (header.Id) { + case HeaderId.Subject: + case HeaderId.MessageId: + case HeaderId.Encrypted: + case HeaderId.MimeVersion: + for (int j = 0; j < headers.Count; j++) { + if (headers[j].Id == header.Id) { + var original = headers[j]; + + joined.Headers.Remove (original); + joined.Headers.Insert (i++, original); + headers.RemoveAt (j); + break; + } + } + break; + default: + var clone = header.Clone (); + clone.Offset = null; + + joined.Headers.Insert (i++, clone); + break; + } + } + + if (joined.Body != null) { + foreach (var header in joined.Body.Headers) + header.Offset = null; + } + } + + static MimeMessage Join (ParserOptions options, MimeMessage message, IEnumerable partials, bool allowNullMessage) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (!allowNullMessage && message == null) + throw new ArgumentNullException (nameof (message)); + + if (partials == null) + throw new ArgumentNullException (nameof (partials)); + + var parts = partials.ToList (); + + if (parts.Count == 0) + return null; + + parts.Sort (PartialCompare); + + if (!parts[parts.Count - 1].Total.HasValue) + throw new ArgumentException ("The last partial does not have a Total.", nameof (partials)); + + int total = parts[parts.Count - 1].Total.Value; + if (parts.Count != total) + throw new ArgumentException ("The number of partials provided does not match the expected count.", nameof (partials)); + + string id = parts[0].Id; + + using (var chained = new ChainedStream ()) { + // chain all of the partial content streams... + for (int i = 0; i < parts.Count; i++) { + int number = parts[i].Number.Value; + + if (number != i + 1) + throw new ArgumentException ("One or more partials is missing.", nameof (partials)); + + var content = parts[i].Content; + + chained.Add (content.Open ()); + } + + var parser = new MimeParser (options, chained); + var joined = parser.ParseMessage (); + + if (message != null) + CombineHeaders (message, joined); + + return joined; + } + } + + /// + /// Joins the specified message/partial parts into the complete message. + /// + /// + /// Combines all of the message/partial fragments into its original, + /// complete, message. + /// + /// The re-combined message. + /// The parser options to use. + /// The message that contains the first `message/partial` part. + /// The list of partial message parts. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The last partial does not have a Total. + /// -or- + /// The number of partials provided does not match the expected count. + /// -or- + /// One or more partials is missing. + /// + public static MimeMessage Join (ParserOptions options, MimeMessage message, IEnumerable partials) + { + return Join (options, message, partials, false); + } + + /// + /// Joins the specified message/partial parts into the complete message. + /// + /// + /// Combines all of the message/partial fragments into its original, + /// complete, message. + /// + /// The re-combined message. + /// The message that contains the first `message/partial` part. + /// The list of partial message parts. + /// + /// is null. + /// -or- + /// is null. + /// + public static MimeMessage Join (MimeMessage message, IEnumerable partials) + { + return Join (ParserOptions.Default, message, partials, false); + } + + /// + /// Joins the specified message/partial parts into the complete message. + /// + /// + /// Combines all of the message/partial fragments into its original, + /// complete, message. + /// + /// The re-combined message. + /// The parser options to use. + /// The list of partial message parts. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The last partial does not have a Total. + /// -or- + /// The number of partials provided does not match the expected count. + /// -or- + /// One or more partials is missing. + /// + [Obsolete ("Use MessagePartial.Join (ParserOptions, MimeMessage, IEnumerable) instead.")] + public static MimeMessage Join (ParserOptions options, IEnumerable partials) + { + return Join (options, null, partials, true); + } + + /// + /// Joins the specified message/partial parts into the complete message. + /// + /// + /// Combines all of the message/partial fragments into its original, + /// complete, message. + /// + /// The re-combined message. + /// The list of partial message parts. + /// + /// is null. + /// + [Obsolete ("Use MessagePartial.Join (MimeMessage, IEnumerable) instead.")] + public static MimeMessage Join (IEnumerable partials) + { + return Join (ParserOptions.Default, null, partials, true); + } + } +} diff --git a/src/MimeKit/MessagePriority.cs b/src/MimeKit/MessagePriority.cs new file mode 100644 index 0000000..e97261f --- /dev/null +++ b/src/MimeKit/MessagePriority.cs @@ -0,0 +1,50 @@ +// +// MessagePriority.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// An enumeration of message priority values. + /// + /// + /// Indicates the priority of a message. + /// + public enum MessagePriority { + /// + /// The message has non-urgent priority. + /// + NonUrgent, + + /// + /// The message has normal priority. + /// + Normal, + + /// + /// The message has urgent priority. + /// + Urgent + } +} diff --git a/src/MimeKit/MimeContent.cs b/src/MimeKit/MimeContent.cs new file mode 100644 index 0000000..27cffac --- /dev/null +++ b/src/MimeKit/MimeContent.cs @@ -0,0 +1,353 @@ +// +// MimeContent.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using MimeKit.IO; +using MimeKit.IO.Filters; + +namespace MimeKit { + /// + /// Encapsulates a content stream used by . + /// + /// + /// A represents the content of a . + /// The content has both a stream and an encoding (typically ). + /// + [Obsolete ("Use the MimeContent class instead.")] + public class ContentObject : MimeContent + { + /// + /// Initialize a new instance of the class. + /// + /// + /// When creating new s, the + /// should typically be unless the + /// has already been encoded. + /// + /// The content stream. + /// The stream encoding. + /// + /// is null. + /// + /// + /// does not support reading. + /// -or- + /// does not support seeking. + /// + [Obsolete ("Use the MimeContent class instead.")] + public ContentObject (Stream stream, ContentEncoding encoding = ContentEncoding.Default) : base (stream, encoding) {} + } + + /// + /// Encapsulates a content stream used by . + /// + /// + /// A represents the content of a . + /// The content has both a stream and an encoding (typically ). + /// + /// + /// + /// + public class MimeContent : IMimeContent + { + /// + /// Initialize a new instance of the class. + /// + /// + /// When creating new s, the + /// should typically be unless the + /// has already been encoded. + /// + /// The content stream. + /// The stream encoding. + /// + /// is null. + /// + /// + /// does not support reading. + /// -or- + /// does not support seeking. + /// + public MimeContent (Stream stream, ContentEncoding encoding = ContentEncoding.Default) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (!stream.CanRead) + throw new ArgumentException ("The stream does not support reading.", nameof (stream)); + + if (!stream.CanSeek) + throw new ArgumentException ("The stream does not support seeking.", nameof (stream)); + + Encoding = encoding; + Stream = stream; + } + + #region IContentObject implementation + + /// + /// Get or set the content encoding. + /// + /// + /// If the was parsed from an existing stream, the + /// encoding will be identical to the , + /// otherwise it will typically be . + /// + /// The content encoding. + public ContentEncoding Encoding { + get; private set; + } + + /// + /// Get the new-line format, if known. + /// + /// + /// This property is typically only set by the as it parses + /// the content of a and is only used as a hint when verifying + /// digital signatures. + /// + /// The new-line format, if known. + public NewLineFormat? NewLineFormat { get; set; } + + /// + /// Get the content stream. + /// + /// + /// Gets the content stream. + /// + /// The stream. + public Stream Stream { + get; private set; + } + + /// + /// Open the decoded content stream. + /// + /// + /// Provides a means of reading the decoded content without having to first write it to another + /// stream using . + /// + /// The decoded content stream. + public Stream Open () + { + Stream.Seek (0, SeekOrigin.Begin); + + var filtered = new FilteredStream (Stream); + filtered.Add (DecoderFilter.Create (Encoding)); + + return filtered; + } + + /// + /// Copy the content stream to the specified output stream. + /// + /// + /// This is equivalent to simply using + /// to copy the content stream to the output stream except that this method is cancellable. + /// If you want the decoded content, use + /// instead. + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var readable = Stream as ICancellableStream; + var writable = stream as ICancellableStream; + var buf = new byte[4096]; + int nread; + + Stream.Seek (0, SeekOrigin.Begin); + + try { + do { + if (readable != null) { + if ((nread = readable.Read (buf, 0, buf.Length, cancellationToken)) <= 0) + break; + } else { + cancellationToken.ThrowIfCancellationRequested (); + if ((nread = Stream.Read (buf, 0, buf.Length)) <= 0) + break; + } + + if (writable != null) { + writable.Write (buf, 0, nread, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (buf, 0, nread); + } + } while (true); + + Stream.Seek (0, SeekOrigin.Begin); + } catch (OperationCanceledException) { + // try and reset the stream + try { + Stream.Seek (0, SeekOrigin.Begin); + } catch (IOException) { + } + + throw; + } + } + + /// + /// Asynchronously copy the content stream to the specified output stream. + /// + /// + /// This is equivalent to simply using + /// to copy the content stream to the output stream except that this method is cancellable. + /// If you want the decoded content, use + /// instead. + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var buf = new byte[4096]; + int nread; + + Stream.Seek (0, SeekOrigin.Begin); + + try { + do { + if ((nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) <= 0) + break; + + await stream.WriteAsync (buf, 0, nread, cancellationToken).ConfigureAwait (false); + } while (true); + + Stream.Seek (0, SeekOrigin.Begin); + } catch (OperationCanceledException) { + // try and reset the stream + try { + Stream.Seek (0, SeekOrigin.Begin); + } catch (IOException) { + } + + throw; + } + } + + /// + /// Decode the content stream into another stream. + /// + /// + /// If the content stream is encoded, this method will decode it into the output stream + /// using a suitable decoder based on the property, otherwise the + /// stream will be copied into the output stream as-is. + /// + /// + /// + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void DecodeTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + using (var filtered = new FilteredStream (stream)) { + filtered.Add (DecoderFilter.Create (Encoding)); + WriteTo (filtered, cancellationToken); + filtered.Flush (cancellationToken); + } + } + + /// + /// Asynchronously decode the content stream into another stream. + /// + /// + /// If the content stream is encoded, this method will decode it into the output stream + /// using a suitable decoder based on the property, otherwise the + /// stream will be copied into the output stream as-is. + /// + /// + /// + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task DecodeToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + using (var filtered = new FilteredStream (stream)) { + filtered.Add (DecoderFilter.Create (Encoding)); + await WriteToAsync (filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + + #endregion + } +} diff --git a/src/MimeKit/MimeEntity.cs b/src/MimeKit/MimeEntity.cs new file mode 100644 index 0000000..a654c28 --- /dev/null +++ b/src/MimeKit/MimeEntity.cs @@ -0,0 +1,1732 @@ +// +// MimeEntity.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// An abstract MIME entity. + /// + /// + /// A MIME entity is really just a node in a tree structure of MIME parts in a MIME message. + /// There are 3 basic types of entities: , , + /// and (which is actually just a special variation of + /// who's content is another MIME message/document). All other types are + /// derivatives of one of those. + /// + public abstract class MimeEntity + { + internal bool EnsureNewLine; + ContentDisposition disposition; + string contentId; + Uri location; + Uri baseUri; + + /// + /// Initialize a new instance of the class + /// based on the . + /// + /// + /// Custom subclasses MUST implement this constructor + /// in order to register it using . + /// + /// Information used by the constructor. + /// + /// is null. + /// + protected MimeEntity (MimeEntityConstructorArgs args) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + Headers = new HeaderList (args.ParserOptions); + ContentType = args.ContentType; + + ContentType.Changed += ContentTypeChanged; + Headers.Changed += HeadersChanged; + + foreach (var header in args.Headers) { + if (args.IsTopLevel && !header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + continue; + + Headers.Add (header); + } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes the based on the provided media type and subtype. + /// + /// The media type. + /// The media subtype. + /// + /// is null. + /// -or- + /// is null. + /// + protected MimeEntity (string mediaType, string mediaSubtype) : this (new ContentType (mediaType, mediaSubtype)) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Initializes the to the one provided. + /// + /// The content type. + /// + /// is null. + /// + protected MimeEntity (ContentType contentType) + { + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + Headers = new HeaderList (); + ContentType = contentType; + + ContentType.Changed += ContentTypeChanged; + Headers.Changed += HeadersChanged; + + SerializeContentType (); + } + + /// + /// Tries to use the given object to initialize the appropriate property. + /// + /// + /// Initializes the appropriate property based on the type of the object. + /// + /// The object. + /// true if the object was recognized and used; false otherwise. + protected bool TryInit (object obj) + { + // The base MimeEntity class only knows about Headers. + if (obj is Header header) { + Headers.Add (header); + return true; + } + + if (obj is IEnumerable
headers) { + foreach (Header h in headers) + Headers.Add (h); + return true; + } + + return false; + } + + /// + /// Gets the list of headers. + /// + /// + /// Represents the list of headers for a MIME part. Typically, the headers of + /// a MIME part will be various Content-* headers such as Content-Type or + /// Content-Disposition, but may include just about anything. + /// + /// The list of headers. + public HeaderList Headers { + get; private set; + } + + /// + /// Gets or sets the content disposition. + /// + /// + /// Represents the pre-parsed Content-Disposition header value, if present. + /// If the Content-Disposition header is not set, then this property will + /// be null. + /// + /// The content disposition. + public ContentDisposition ContentDisposition { + get { return disposition; } + set { + if (disposition == value) + return; + + if (disposition != null) { + disposition.Changed -= ContentDispositionChanged; + RemoveHeader ("Content-Disposition"); + } + + disposition = value; + if (disposition != null) { + disposition.Changed += ContentDispositionChanged; + SerializeContentDisposition (); + } + } + } + + /// + /// Gets the type of the content. + /// + /// + /// The Content-Type header specifies information about the type of content contained + /// within the MIME entity. + /// + /// The type of the content. + public ContentType ContentType { + get; private set; + } + + /// + /// Gets or sets the base content URI. + /// + /// + /// The Content-Base header specifies the base URI for the + /// in cases where the is a relative URI. + /// The Content-Base URI must be an absolute URI. + /// For more information, see rfc2110. + /// + /// The base content URI or null. + /// + /// is not an absolute URI. + /// + public Uri ContentBase { + get { return baseUri; } + set { + if (baseUri == value) + return; + + if (value != null && !value.IsAbsoluteUri) + throw new ArgumentException ("The Content-Base URI may only be set to an absolute URI.", nameof (value)); + + baseUri = value; + + if (value != null) + SetHeader ("Content-Base", value.ToString ()); + else + RemoveHeader ("Content-Base"); + } + } + + /// + /// Gets or sets the content location. + /// + /// + /// The Content-Location header specifies the URI for a MIME entity and can be + /// either absolute or relative. + /// Setting a Content-Location URI allows other objects + /// within the same multipart/related container to reference this part by URI. This + /// can be useful, for example, when constructing an HTML message body that needs to + /// reference image attachments. + /// For more information, see rfc2110. + /// + /// The content location or null. + public Uri ContentLocation { + get { return location; } + set { + if (location == value) + return; + + location = value; + + if (value != null) + SetHeader ("Content-Location", value.ToString ()); + else + RemoveHeader ("Content-Location"); + } + } + + /// + /// Gets or sets the content identifier. + /// + /// + /// The Content-Id header is used for uniquely identifying a particular entity and + /// uses the same syntax as the Message-Id header on MIME messages. + /// Setting a Content-Id allows other objects within the same + /// multipart/related container to reference this part by its unique identifier, typically + /// by using a "cid:" URI in an HTML-formatted message body. This can be useful, for example, + /// when the HTML-formatted message body needs to reference image attachments. + /// + /// The content identifier. + public string ContentId { + get { return contentId; } + set { + if (contentId == value) + return; + + if (value == null) { + RemoveHeader ("Content-Id"); + contentId = null; + return; + } + + var buffer = Encoding.UTF8.GetBytes (value); + int index = 0; + + if (!ParseUtils.TryParseMsgId (buffer, ref index, buffer.Length, false, false, out string id)) + throw new ArgumentException ("Invalid Content-Id format.", nameof (value)); + + contentId = id; + + SetHeader ("Content-Id", "<" + contentId + ">"); + } + } + + /// + /// Gets a value indicating whether this is an attachment. + /// + /// + /// If the Content-Disposition header is set and has a value of "attachment", + /// then this property returns true. Otherwise it is assumed that the + /// is not meant to be treated as an attachment. + /// + /// true if this is an attachment; otherwise, false. + public bool IsAttachment { + get { return ContentDisposition != null && ContentDisposition.IsAttachment; } + set { + if (value) { + if (ContentDisposition == null) + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + else if (!ContentDisposition.IsAttachment) + ContentDisposition.Disposition = ContentDisposition.Attachment; + } else if (ContentDisposition != null && ContentDisposition.IsAttachment) { + ContentDisposition.Disposition = ContentDisposition.Inline; + } + } + } + + /// + /// Returns a that represents the for debugging purposes. + /// + /// + /// Returns a that represents the for debugging purposes. + /// In general, the string returned from this method SHOULD NOT be used for serializing + /// the entity to disk. It is recommended that you use instead. + /// If this method is used for serializing the entity to disk, the iso-8859-1 text encoding should be used for + /// conversion. + /// + /// A that represents the for debugging purposes. + public override string ToString () + { + using (var memory = new MemoryStream ()) { + WriteTo (memory); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var buffer = memory.GetBuffer (); +#else + var buffer = memory.ToArray (); +#endif + int count = (int) memory.Length; + + return CharsetUtils.Latin1.GetString (buffer, 0, count); + } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public virtual void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMimeEntity (this); + } + + /// + /// Prepare the MIME entity for transport using the specified encoding constraints. + /// + /// + /// Prepares the MIME entity for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum allowable length for a line (not counting the CRLF). Must be between 72 and 998 (inclusive). + /// + /// is not between 72 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public abstract void Prepare (EncodingConstraint constraint, int maxLineLength = 78); + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the headers to the output stream, followed by a blank line. + /// Subclasses should override this method to write the content of the entity. + /// + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public virtual void WriteTo (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (!contentOnly) + Headers.WriteTo (options, stream, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the headers to the output stream, followed by a blank line. + /// Subclasses should override this method to write the content of the entity. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public virtual Task WriteToAsync (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (!contentOnly) + return Headers.WriteToAsync (options, stream, cancellationToken); + + return Task.FromResult (0); + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the headers to the output stream, followed by a blank line. + /// Subclasses should override this method to write the content of the entity. + /// + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (options, stream, false, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the headers to the output stream, followed by a blank line. + /// Subclasses should override this method to write the content of the entity. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (options, stream, false, cancellationToken); + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the entity to the output stream. + /// + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, stream, contentOnly, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the entity to the output stream. + /// + /// An awaitable task. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, stream, contentOnly, cancellationToken); + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the entity to the output stream. + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, stream, false, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the entity to the output stream. + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, stream, false, cancellationToken); + } + + /// + /// Write the to the specified file. + /// + /// + /// Writes the entity to the specified file using the provided formatting options. + /// + /// The formatting options. + /// The file. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, string fileName, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) + WriteTo (options, stream, contentOnly, cancellationToken); + } + + /// + /// Asynchronously write the to the specified file. + /// + /// + /// Asynchronously writes the entity to the specified file using the provided formatting options. + /// + /// An awaitable task. + /// The formatting options. + /// The file. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (FormatOptions options, string fileName, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) + await WriteToAsync (options, stream, contentOnly, cancellationToken).ConfigureAwait (false); + } + + /// + /// Write the to the specified file. + /// + /// + /// Writes the entity to the specified file using the provided formatting options. + /// + /// The formatting options. + /// The file. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) { + WriteTo (options, stream, false, cancellationToken); + stream.Flush (); + } + } + + /// + /// Asynchronously write the to the specified file. + /// + /// + /// Asynchronously writes the entity to the specified file using the provided formatting options. + /// + /// An awaitable task. + /// The formatting options. + /// The file. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (FormatOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) { + await WriteToAsync (options, stream, false, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + + /// + /// Write the to the specified file. + /// + /// + /// Writes the entity to the specified file using the default formatting options. + /// + /// The file. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (string fileName, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, fileName, contentOnly, cancellationToken); + } + + /// + /// Asynchronously write the to the specified file. + /// + /// + /// Asynchronously writes the entity to the specified file using the default formatting options. + /// + /// An awaitable task. + /// The file. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (string fileName, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, fileName, contentOnly, cancellationToken); + } + + /// + /// Write the to the specified file. + /// + /// + /// Writes the entity to the specified file using the default formatting options. + /// + /// The file. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, fileName, cancellationToken); + } + + /// + /// Asynchronously write the to the specified file. + /// + /// + /// Asynchronously writes the entity to the specified file using the default formatting options. + /// + /// An awaitable task. + /// The file. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, fileName, cancellationToken); + } + + /// + /// Remove a header by name. + /// + /// + /// Removes all headers matching the specified name without + /// calling . + /// + /// The name of the header. + protected void RemoveHeader (string name) + { + Headers.Changed -= HeadersChanged; + + try { + Headers.RemoveAll (name); + } finally { + Headers.Changed += HeadersChanged; + } + } + + /// + /// Set the value of a header. + /// + /// + /// Sets the header to the specified value without + /// calling . + /// + /// The name of the header. + /// The value of the header. + protected void SetHeader (string name, string value) + { + Headers.Changed -= HeadersChanged; + + try { + Headers[name] = value; + } finally { + Headers.Changed += HeadersChanged; + } + } + + /// + /// Set the value of a header using the raw value. + /// + /// + /// Sets the header to the specified value without + /// calling . + /// + /// The name of the header. + /// The raw value of the header. + protected void SetHeader (string name, byte[] rawValue) + { + var header = new Header (Headers.Options, name.ToHeaderId (), name, rawValue); + + Headers.Changed -= HeadersChanged; + + try { + Headers.Replace (header); + } finally { + Headers.Changed += HeadersChanged; + } + } + + void SerializeContentDisposition () + { + var text = disposition.Encode (FormatOptions.Default, Encoding.UTF8); + var raw = Encoding.UTF8.GetBytes (text); + + SetHeader ("Content-Disposition", raw); + } + + void SerializeContentType () + { + var text = ContentType.Encode (FormatOptions.Default, Encoding.UTF8); + var raw = Encoding.UTF8.GetBytes (text); + + SetHeader ("Content-Type", raw); + } + + void ContentDispositionChanged (object sender, EventArgs e) + { + SerializeContentDisposition (); + } + + void ContentTypeChanged (object sender, EventArgs e) + { + SerializeContentType (); + } + + /// + /// Called when the headers change in some way. + /// + /// + /// Whenever a header is added, changed, or removed, this method will + /// be called in order to allow custom subclasses + /// to update their state. + /// Overrides of this method should call the base method so that their + /// superclass may also update its own state. + /// + /// The type of change. + /// The header being added, changed or removed. + protected virtual void OnHeadersChanged (HeaderListChangedAction action, Header header) + { + int index = 0; + string text; + + switch (action) { + case HeaderListChangedAction.Added: + case HeaderListChangedAction.Changed: + switch (header.Id) { + case HeaderId.ContentDisposition: + if (disposition != null) + disposition.Changed -= ContentDispositionChanged; + + if (ContentDisposition.TryParse (Headers.Options, header.RawValue, out disposition)) + disposition.Changed += ContentDispositionChanged; + break; + case HeaderId.ContentLocation: + text = header.Value.Trim (); + + if (Uri.IsWellFormedUriString (text, UriKind.Absolute)) + location = new Uri (text, UriKind.Absolute); + else if (Uri.IsWellFormedUriString (text, UriKind.Relative)) + location = new Uri (text, UriKind.Relative); + else + location = null; + break; + case HeaderId.ContentBase: + text = header.Value.Trim (); + + if (Uri.IsWellFormedUriString (text, UriKind.Absolute)) + baseUri = new Uri (text, UriKind.Absolute); + else + baseUri = null; + break; + case HeaderId.ContentId: + if (ParseUtils.TryParseMsgId (header.RawValue, ref index, header.RawValue.Length, false, false, out string msgid)) + contentId = msgid; + else + contentId = null; + break; + } + break; + case HeaderListChangedAction.Removed: + switch (header.Id) { + case HeaderId.ContentDisposition: + if (disposition != null) + disposition.Changed -= ContentDispositionChanged; + + disposition = null; + break; + case HeaderId.ContentLocation: + location = null; + break; + case HeaderId.ContentBase: + baseUri = null; + break; + case HeaderId.ContentId: + contentId = null; + break; + } + break; + case HeaderListChangedAction.Cleared: + if (disposition != null) + disposition.Changed -= ContentDispositionChanged; + + disposition = null; + contentId = null; + location = null; + baseUri = null; + break; + default: + throw new ArgumentOutOfRangeException (nameof (action)); + } + } + + void HeadersChanged (object sender, HeaderListChangedEventArgs e) + { + OnHeadersChanged (e.Action, e.Header); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed MIME entity. + /// The parser options. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (ParserOptions options, Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity, persistent); + + return parser.ParseEntity (cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed MIME entity. + /// The parser options. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity, persistent); + + return parser.ParseEntityAsync (cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed MIME entity. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (options, stream, false, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed MIME entity. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (options, stream, false, cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed MIME entity. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, stream, persistent, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed MIME entity. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, stream, persistent, cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed MIME entity. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, stream, false, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed MIME entity. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, stream, false, cancellationToken); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the specified . + /// + /// The parsed entity. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return Load (options, stream, cancellationToken); + } + + /// + /// Asynchronously load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the specified . + /// + /// The parsed entity. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static async Task LoadAsync (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return await LoadAsync (options, stream, cancellationToken).ConfigureAwait (false); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the default . + /// + /// The parsed entity. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, fileName, cancellationToken); + } + + /// + /// Asynchroinously load a from the specified file. + /// + /// + /// Loads a from the file at the give file path, + /// using the default . + /// + /// The parsed entity. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, fileName, cancellationToken); + } + + /// + /// Load a from the specified content stream. + /// + /// + /// This method is mostly meant for use with APIs such as + /// where the headers are parsed separately from the content. + /// + /// The parsed MIME entity. + /// The parser options. + /// The Content-Type of the stream. + /// The content stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (ParserOptions options, ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var format = FormatOptions.CloneDefault (); + format.NewLineFormat = NewLineFormat.Dos; + + var encoded = contentType.Encode (format, Encoding.UTF8); + var header = string.Format ("Content-Type:{0}\r\n", encoded); + var chained = new ChainedStream (); + + chained.Add (new MemoryStream (Encoding.UTF8.GetBytes (header), false)); + chained.Add (content); + + return Load (options, chained, cancellationToken); + } + + /// + /// Asynchronously load a from the specified content stream. + /// + /// + /// This method is mostly meant for use with APIs such as + /// where the headers are parsed separately from the content. + /// + /// The parsed MIME entity. + /// The parser options. + /// The Content-Type of the stream. + /// The content stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (contentType == null) + throw new ArgumentNullException (nameof (contentType)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var format = FormatOptions.CloneDefault (); + format.NewLineFormat = NewLineFormat.Dos; + + var encoded = contentType.Encode (format, Encoding.UTF8); + var header = string.Format ("Content-Type:{0}\r\n", encoded); + var chained = new ChainedStream (); + + chained.Add (new MemoryStream (Encoding.UTF8.GetBytes (header), false)); + chained.Add (content); + + return LoadAsync (options, chained, cancellationToken); + } + + /// + /// Load a from the specified content stream. + /// + /// + /// This method is mostly meant for use with APIs such as + /// where the headers are parsed separately from the content. + /// + /// + /// + /// + /// The parsed MIME entity. + /// The Content-Type of the stream. + /// The content stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeEntity Load (ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, contentType, content, cancellationToken); + } + + /// + /// Asynchronously load a from the specified content stream. + /// + /// + /// This method is mostly meant for use with APIs such as + /// where the headers are parsed separately from the content. + /// + /// The parsed MIME entity. + /// The Content-Type of the stream. + /// The content stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ContentType contentType, Stream content, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, contentType, content, cancellationToken); + } + } +} diff --git a/src/MimeKit/MimeEntityConstructorArgs.cs b/src/MimeKit/MimeEntityConstructorArgs.cs new file mode 100644 index 0000000..e2896bc --- /dev/null +++ b/src/MimeKit/MimeEntityConstructorArgs.cs @@ -0,0 +1,51 @@ +// +// MimeEntityConstructorArgs.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.Collections.Generic; + +namespace MimeKit { + /// + /// MIME entity constructor arguments. + /// + /// + /// MIME entity constructor arguments. + /// + public sealed class MimeEntityConstructorArgs + { + internal readonly ParserOptions ParserOptions; + internal readonly IEnumerable
Headers; + internal readonly ContentType ContentType; + internal readonly bool IsTopLevel; + + internal MimeEntityConstructorArgs (ParserOptions options, ContentType ctype, IEnumerable
headers, bool toplevel) + { + ParserOptions = options; + IsTopLevel = toplevel; + ContentType = ctype; + Headers = headers; + } + } +} diff --git a/src/MimeKit/MimeFormat.cs b/src/MimeKit/MimeFormat.cs new file mode 100644 index 0000000..30a1692 --- /dev/null +++ b/src/MimeKit/MimeFormat.cs @@ -0,0 +1,51 @@ +// +// MimeFormat.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// The format of the MIME stream. + /// + /// + /// The format of the MIME stream. + /// + public enum MimeFormat : byte { + /// + /// The stream contains a single MIME entity or message. + /// + Entity, + + /// + /// The stream is in the Unix mbox format and may contain + /// more than a single message. + /// + Mbox, + + /// + /// The default stream format. + /// + Default = Entity, + } +} diff --git a/src/MimeKit/MimeIterator.cs b/src/MimeKit/MimeIterator.cs new file mode 100644 index 0000000..1503d43 --- /dev/null +++ b/src/MimeKit/MimeIterator.cs @@ -0,0 +1,455 @@ +// +// MimeIterator.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +namespace MimeKit { + /// + /// An iterator for a MIME tree structure. + /// + /// + /// Walks the MIME tree structure of a in depth-first order. + /// + /// + /// + /// + public class MimeIterator : IEnumerator + { + class MimeNode + { + public readonly MimeEntity Entity; + public readonly bool Indexed; + + public MimeNode (MimeEntity entity, bool indexed) + { + Entity = entity; + Indexed = indexed; + } + } + + readonly Stack stack = new Stack (); + readonly List path = new List (); + bool moveFirst = true; + MimeEntity current; + int index = -1; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new for the specified message. + /// + /// + /// + /// + /// The message. + /// + /// is null. + /// + public MimeIterator (MimeMessage message) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + Message = message; + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before + /// the is reclaimed by garbage collection. + /// + /// + /// Releases unmanaged resources and performs other cleanup operations before + /// the is reclaimed by garbage collection. + /// + ~MimeIterator () + { + Dispose (false); + } + + /// + /// Gets the top-level message. + /// + /// + /// Gets the top-level message. + /// + /// The message. + public MimeMessage Message { + get; private set; + } + + /// + /// Gets the parent of the current entity. + /// + /// + /// After an iterator is created or after the method is called, + /// the method must be called to advance the iterator to the + /// first entity of the message before reading the value of the Parent property; + /// otherwise, Parent throws a . Parent + /// also throws a if the last call to + /// returned false, which indicates the end of the message. + /// If the current entity is the top-level entity of the message, then the parent + /// will be null; otherwise the parent will be either be a + /// or a . + /// + /// + /// + /// + /// The parent entity. + /// + /// Either has not been called or + /// has moved beyond the end of the message. + /// + public MimeEntity Parent { + get { + if (current == null) + throw new InvalidOperationException (); + + return stack.Count > 0 ? stack.Peek ().Entity : null; + } + } + + /// + /// Gets the current entity. + /// + /// + /// After an iterator is created or after the method is called, + /// the method must be called to advance the iterator to the + /// first entity of the message before reading the value of the Current property; + /// otherwise, Current throws a . Current + /// also throws a if the last call to + /// returned false, which indicates the end of the message. + /// + /// + /// + /// + /// The current entity. + /// + /// Either has not been called or + /// has moved beyond the end of the message. + /// + public MimeEntity Current { + get { + if (current == null) + throw new InvalidOperationException (); + + return current; + } + } + + /// + /// Gets the current entity. + /// + /// + /// After an iterator is created or after the method is called, + /// the method must be called to advance the iterator to the + /// first entity of the message before reading the value of the Current property; + /// otherwise, Current throws a . Current + /// also throws a if the last call to + /// returned false, which indicates the end of the message. + /// + /// The current entity. + /// + /// Either has not been called or + /// has moved beyond the end of the message. + /// + object IEnumerator.Current { + get { return Current; } + } + + /// + /// Gets the path specifier for the current entity. + /// + /// + /// After an iterator is created or after the method is called, + /// the method must be called to advance the iterator to the + /// first entity of the message before reading the value of the PathSpecifier property; + /// otherwise, PathSpecifier throws a . + /// PathSpecifier also throws a if the + /// last call to returned false, which indicates the end of + /// the message. + /// + /// The path specifier. + /// + /// Either has not been called or + /// has moved beyond the end of the message. + /// + public string PathSpecifier { + get { + if (current == null) + throw new InvalidOperationException (); + + var specifier = new StringBuilder (); + + for (int i = 0; i < path.Count; i++) + specifier.AppendFormat ("{0}.", path[i] + 1); + + specifier.AppendFormat ("{0}", index + 1); + + return specifier.ToString (); + } + } + + /// + /// Gets the depth of the current entity. + /// + /// + /// After an iterator is created or after the method is called, + /// the method must be called to advance the iterator to the + /// first entity of the message before reading the value of the Depth property; + /// otherwise, Depth throws a . Depth + /// also throws a if the last call to + /// returned false, which indicates the end of the message. + /// + /// The depth. + /// + /// Either has not been called or + /// has moved beyond the end of the message. + /// + public int Depth { + get { + if (current == null) + throw new InvalidOperationException (); + + return stack.Count; + } + } + + void Push (MimeEntity entity) + { + if (index != -1) + path.Add (index); + + stack.Push (new MimeNode (entity, index != -1)); + } + + bool Pop () + { + if (stack.Count == 0) + return false; + + var node = stack.Pop (); + + if (node.Indexed) { + index = path[path.Count - 1]; + path.RemoveAt (path.Count - 1); + } + + current = node.Entity; + + return true; + } + + /// + /// Advances the iterator to the next depth-first entity of the tree structure. + /// + /// + /// After an iterator is created or after the method is called, + /// an iterator is positioned before the first entity of the message, and the first + /// call to the MoveNext method moves the iterator to the first entity of the message. + /// If MoveNext advances beyond the last entity of the message, MoveNext returns false. + /// When the iterator is at this position, subsequent calls to MoveNext also return + /// false until is called. + /// + /// + /// + /// + /// true if the iterator was successfully advanced to the next entity; otherwise, false. + public bool MoveNext () + { + if (moveFirst) { + current = Message.Body; + moveFirst = false; + + return current != null; + } + + var message_part = current as MessagePart; + var multipart = current as Multipart; + + if (message_part != null) { + current = message_part.Message != null ? message_part.Message.Body : null; + + if (current != null) { + Push (message_part); + index = current is Multipart ? -1 : 0; + return true; + } + } + + if (multipart != null) { + if (multipart.Count > 0) { + Push (current); + current = multipart[0]; + index = 0; + return true; + } + } + + // find the next sibling + while (stack.Count > 0) { + multipart = stack.Peek ().Entity as Multipart; + + if (multipart != null) { + // advance to the next part in the multipart... + if (multipart.Count > ++index) { + current = multipart[index]; + return true; + } + } + + if (!Pop ()) + break; + } + + current = null; + index = -1; + + return false; + } + + static int[] Parse (string pathSpecifier) + { + var path = pathSpecifier.Split ('.'); + var indexes = new int[path.Length]; + int index; + + for (int i = 0; i < path.Length; i++) { + if (!int.TryParse (path[i], out index) || index < 0) + throw new FormatException ("Invalid path specifier format."); + + indexes[i] = index - 1; + } + + return indexes; + } + + /// + /// Advances to the entity specified by the path specifier. + /// + /// + /// Advances the iterator to the entity specified by the path specifier which + /// must be in the same format as returned by . + /// If the iterator has already advanced beyond the entity at the specified + /// path, the iterator will and advance as normal. + /// + /// true if advancing to the specified entity was successful; otherwise, false. + /// The path specifier. + /// + /// is null. + /// + /// + /// is empty. + /// + /// + /// is in an invalid format. + /// + public bool MoveTo (string pathSpecifier) + { + if (pathSpecifier == null) + throw new ArgumentNullException (nameof (pathSpecifier)); + + if (pathSpecifier.Length == 0) + throw new ArgumentException ("The path specifier cannot be empty.", nameof (pathSpecifier)); + + var indexes = Parse (pathSpecifier); + int i; + + // OPTIMIZATION: only reset the iterator if we are jumping to a previous part + for (i = 0; i < Math.Min (indexes.Length, path.Count); i++) { + if (indexes[i] < path[i]) { + Reset (); + break; + } + } + + if (!moveFirst && indexes.Length < path.Count) + Reset (); + + if (moveFirst && !MoveNext ()) + return false; + + do { + if (path.Count + 1 == indexes.Length) { + for (i = 0; i < path.Count; i++) { + if (indexes[i] != path[i]) + break; + } + + if (i == path.Count && indexes[i] == index) + return true; + } + } while (MoveNext ()); + + return false; + } + + /// + /// Resets the iterator to its initial state. + /// + /// + /// Resets the iterator to its initial state. + /// + public void Reset () + { + moveFirst = true; + current = null; + stack.Clear (); + path.Clear (); + index = -1; + } + + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// + /// Releases the unmanaged resources used by the and + /// optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only the unmanaged resources. + protected virtual void Dispose (bool disposing) + { + } + + /// + /// Releases all resources used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + } +} diff --git a/src/MimeKit/MimeMessage.cs b/src/MimeKit/MimeMessage.cs new file mode 100644 index 0000000..35fada6 --- /dev/null +++ b/src/MimeKit/MimeMessage.cs @@ -0,0 +1,3185 @@ +// +// MimeMessage.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Linq; +using System.Threading; +using System.Globalization; +using System.Threading.Tasks; +using System.Collections.Generic; + +#if ENABLE_SNM +using System.Net.Mail; +#endif + +#if ENABLE_CRYPTO +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto.Parameters; + +using MimeKit.Cryptography; +#endif + +using MimeKit.IO; +using MimeKit.Text; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A MIME message. + /// + /// + /// A message consists of header fields and, optionally, a body. + /// The body of the message can either be plain text or it can be a + /// tree of MIME entities such as a text/plain MIME part and a collection + /// of file attachments. + /// + public class MimeMessage + { + static readonly string[] StandardAddressHeaders = { + "Resent-From", "Resent-Reply-To", "Resent-To", "Resent-Cc", "Resent-Bcc", + "From", "Reply-To", "To", "Cc", "Bcc" + }; + + readonly Dictionary addresses; + MessageImportance importance = MessageImportance.Normal; + XMessagePriority xpriority = XMessagePriority.Normal; + MessagePriority priority = MessagePriority.Normal; + readonly RfcComplianceMode compliance; + readonly MessageIdList references; + MailboxAddress resentSender; + DateTimeOffset resentDate; + string resentMessageId; + MailboxAddress sender; + DateTimeOffset date; + string messageId; + string inreplyto; + Version version; + + // Note: this .ctor is used only by the MimeParser and MimeMessage.CreateFromMailMessage() + internal MimeMessage (ParserOptions options, IEnumerable
headers, RfcComplianceMode mode) + { + addresses = new Dictionary (MimeUtils.OrdinalIgnoreCase); + Headers = new HeaderList (options); + + compliance = mode; + + // initialize our address lists + foreach (var name in StandardAddressHeaders) { + var list = new InternetAddressList (); + list.Changed += InternetAddressListChanged; + addresses.Add (name, list); + } + + references = new MessageIdList (); + references.Changed += ReferencesChanged; + inreplyto = null; + + Headers.Changed += HeadersChanged; + + // add all of our message headers... + foreach (var header in headers) { + if (header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + continue; + + Headers.Add (header); + } + } + + internal MimeMessage (ParserOptions options) + { + addresses = new Dictionary (MimeUtils.OrdinalIgnoreCase); + Headers = new HeaderList (options); + + compliance = RfcComplianceMode.Strict; + + // initialize our address lists + foreach (var name in StandardAddressHeaders) { + var list = new InternetAddressList (); + list.Changed += InternetAddressListChanged; + addresses.Add (name, list); + } + + references = new MessageIdList (); + references.Changed += ReferencesChanged; + inreplyto = null; + + Headers.Changed += HeadersChanged; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// An array of initialization parameters: headers and message parts. + /// + /// is null. + /// + /// + /// contains more than one . + /// -or- + /// contains one or more arguments of an unknown type. + /// + public MimeMessage (params object[] args) : this (ParserOptions.Default.Clone ()) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + MimeEntity body = null; + + foreach (object obj in args) { + if (obj == null) + continue; + + // Just add the headers and let the events (already setup) keep the + // addresses in sync. + + var header = obj as Header; + if (header != null) { + if (!header.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + Headers.Add (header); + + continue; + } + + var headers = obj as IEnumerable
; + if (headers != null) { + foreach (var h in headers) { + if (!h.Field.StartsWith ("Content-", StringComparison.OrdinalIgnoreCase)) + Headers.Add (h); + } + + continue; + } + + var entity = obj as MimeEntity; + if (entity != null) { + if (body != null) + throw new ArgumentException ("Message body should not be specified more than once."); + + body = entity; + continue; + } + + throw new ArgumentException ("Unknown initialization parameter: " + obj.GetType ()); + } + + if (body != null) + Body = body; + + // Do exactly as in the parameterless constructor but avoid setting a default + // value if an header already provided one. + + if (!Headers.Contains (HeaderId.From)) + Headers[HeaderId.From] = string.Empty; + if (date == default (DateTimeOffset)) + Date = DateTimeOffset.Now; + if (!Headers.Contains (HeaderId.Subject)) + Subject = string.Empty; + if (messageId == null) + MessageId = MimeUtils.GenerateMessageId (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new MIME message, specifying details at creation time. + /// + /// The list of addresses in the From header. + /// The list of addresses in the To header. + /// The subject of the message. + /// The body of the message. + public MimeMessage (IEnumerable from, IEnumerable to, string subject, MimeEntity body) : this () + { + From.AddRange (from); + To.AddRange (to); + Subject = subject; + Body = body; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new MIME message. + /// + public MimeMessage () : this (ParserOptions.Default.Clone ()) + { + Headers[HeaderId.From] = string.Empty; + Date = DateTimeOffset.Now; + Subject = string.Empty; + MessageId = MimeUtils.GenerateMessageId (); + } + + /// + /// Get or set the mbox marker. + /// + /// + /// Set by the when parsing attached message/rfc822 parts + /// so that the message/rfc822 part can be reserialized back to its original form. + /// + /// The mbox marker. + internal byte[] MboxMarker { + get; set; + } + + /// + /// Get the list of headers. + /// + /// + /// Represents the list of headers for a message. Typically, the headers of + /// a message will contain transmission headers such as From and To along + /// with metadata headers such as Subject and Date, but may include just + /// about anything. + /// To access any MIME headers other than + /// , you will need to access the + /// property of the . + /// + /// + /// The list of headers. + public HeaderList Headers { + get; private set; + } + + /// + /// Get or set the value of the Importance header. + /// + /// + /// Gets or sets the value of the Importance header. + /// + /// The importance. + /// + /// is not a valid . + /// + public MessageImportance Importance { + get { return importance; } + set { + if (value == importance) + return; + + switch (value) { + case MessageImportance.Normal: + case MessageImportance.High: + case MessageImportance.Low: + SetHeader ("Importance", value.ToString ().ToLowerInvariant ()); + importance = value; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + } + } + + /// + /// Get or set the value of the Priority header. + /// + /// + /// Gets or sets the value of the Priority header. + /// + /// The priority. + /// + /// is not a valid . + /// + public MessagePriority Priority { + get { return priority; } + set { + if (value == priority) + return; + + string rawValue; + + switch (value) { + case MessagePriority.NonUrgent: + rawValue = "non-urgent"; + break; + case MessagePriority.Normal: + rawValue = "normal"; + break; + case MessagePriority.Urgent: + rawValue = "urgent"; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + + SetHeader ("Priority", rawValue); + + priority = value; + } + } + + /// + /// Get or set the value of the X-Priority header. + /// + /// + /// Gets or sets the value of the X-Priority header. + /// + /// The priority. + /// + /// is not a valid . + /// + public XMessagePriority XPriority { + get { return xpriority; } + set { + if (value == xpriority) + return; + + string rawValue; + + switch (value) { + case XMessagePriority.Highest: + rawValue = "1 (Highest)"; + break; + case XMessagePriority.High: + rawValue = "2 (High)"; + break; + case XMessagePriority.Normal: + rawValue = "3 (Normal)"; + break; + case XMessagePriority.Low: + rawValue = "4 (Low)"; + break; + case XMessagePriority.Lowest: + rawValue = "5 (Lowest)"; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + + SetHeader ("X-Priority", rawValue); + + xpriority = value; + } + } + + /// + /// Get or set the address in the Sender header. + /// + /// + /// The sender may differ from the addresses in if + /// the message was sent by someone on behalf of someone else. + /// + /// The address in the Sender header. + public MailboxAddress Sender { + get { return sender; } + set { + if (value == sender) + return; + + if (value == null) { + RemoveHeader (HeaderId.Sender); + sender = null; + return; + } + + var options = FormatOptions.Default; + var builder = new StringBuilder (" "); + int len = "Sender: ".Length; + + value.Encode (options, builder, true, ref len); + builder.Append (options.NewLine); + + var raw = Encoding.UTF8.GetBytes (builder.ToString ()); + + ReplaceHeader (HeaderId.Sender, "Sender", raw); + + sender = value; + } + } + + /// + /// Get or set the address in the Resent-Sender header. + /// + /// + /// The resent sender may differ from the addresses in if + /// the message was sent by someone on behalf of someone else. + /// + /// The address in the Resent-Sender header. + public MailboxAddress ResentSender { + get { return resentSender; } + set { + if (value == resentSender) + return; + + if (value == null) { + RemoveHeader (HeaderId.ResentSender); + resentSender = null; + return; + } + + var options = FormatOptions.Default; + var builder = new StringBuilder (" "); + int len = "Resent-Sender: ".Length; + + value.Encode (options, builder, true, ref len); + builder.Append (options.NewLine); + + var raw = Encoding.UTF8.GetBytes (builder.ToString ()); + + ReplaceHeader (HeaderId.ResentSender, "Resent-Sender", raw); + + resentSender = value; + } + } + + /// + /// Get the list of addresses in the From header. + /// + /// + /// The "From" header specifies the author(s) of the message. + /// If more than one is added to the + /// list of "From" addresses, the should be set to the + /// single of the personal actually sending + /// the message. + /// + /// The list of addresses in the From header. + public InternetAddressList From { + get { return addresses["From"]; } + } + + /// + /// Get the list of addresses in the Resent-From header. + /// + /// + /// The "Resent-From" header specifies the author(s) of the messagebeing + /// resent. + /// If more than one is added to the + /// list of "Resent-From" addresses, the should + /// be set to the single of the personal actually + /// sending the message. + /// + /// The list of addresses in the Resent-From header. + public InternetAddressList ResentFrom { + get { return addresses["Resent-From"]; } + } + + /// + /// Get the list of addresses in the Reply-To header. + /// + /// + /// When the list of addresses in the Reply-To header is not empty, + /// it contains the address(es) where the author(s) of the message prefer + /// that replies be sent. + /// When the list of addresses in the Reply-To header is empty, + /// replies should be sent to the mailbox(es) specified in the From + /// header. + /// + /// The list of addresses in the Reply-To header. + public InternetAddressList ReplyTo { + get { return addresses["Reply-To"]; } + } + + /// + /// Get the list of addresses in the Resent-Reply-To header. + /// + /// + /// When the list of addresses in the Resent-Reply-To header is not empty, + /// it contains the address(es) where the author(s) of the resent message prefer + /// that replies be sent. + /// When the list of addresses in the Resent-Reply-To header is empty, + /// replies should be sent to the mailbox(es) specified in the Resent-From + /// header. + /// + /// The list of addresses in the Resent-Reply-To header. + public InternetAddressList ResentReplyTo { + get { return addresses["Resent-Reply-To"]; } + } + + /// + /// Get the list of addresses in the To header. + /// + /// + /// The addresses in the To header are the primary recipients of + /// the message. + /// + /// The list of addresses in the To header. + public InternetAddressList To { + get { return addresses["To"]; } + } + + /// + /// Get the list of addresses in the Resent-To header. + /// + /// + /// The addresses in the Resent-To header are the primary recipients of + /// the message. + /// + /// The list of addresses in the Resent-To header. + public InternetAddressList ResentTo { + get { return addresses["Resent-To"]; } + } + + /// + /// Get the list of addresses in the Cc header. + /// + /// + /// The addresses in the Cc header are secondary recipients of the message + /// and are usually not the individuals being directly addressed in the + /// content of the message. + /// + /// The list of addresses in the Cc header. + public InternetAddressList Cc { + get { return addresses["Cc"]; } + } + + /// + /// Get the list of addresses in the Resent-Cc header. + /// + /// + /// The addresses in the Resent-Cc header are secondary recipients of the message + /// and are usually not the individuals being directly addressed in the + /// content of the message. + /// + /// The list of addresses in the Resent-Cc header. + public InternetAddressList ResentCc { + get { return addresses["Resent-Cc"]; } + } + + /// + /// Get the list of addresses in the Bcc header. + /// + /// + /// Recipients in the Blind-Carpbon-Copy list will not be visible to + /// the other recipients of the message. + /// + /// The list of addresses in the Bcc header. + public InternetAddressList Bcc { + get { return addresses["Bcc"]; } + } + + /// + /// Get the list of addresses in the Resent-Bcc header. + /// + /// + /// Recipients in the Resent-Bcc list will not be visible to + /// the other recipients of the message. + /// + /// The list of addresses in the Resent-Bcc header. + public InternetAddressList ResentBcc { + get { return addresses["Resent-Bcc"]; } + } + + /// + /// Get or set the subject of the message. + /// + /// + /// The Subject is typically a short string denoting the topic of the message. + /// Replies will often use "Re: " followed by the Subject of the original message. + /// + /// The subject of the message. + /// + /// is null. + /// + public string Subject { + get { return Headers["Subject"]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + SetHeader ("Subject", value); + } + } + + /// + /// Get or set the date of the message. + /// + /// + /// If the date is not explicitly set before the message is written to a stream, + /// the date will default to the exact moment when it is written to said stream. + /// + /// The date of the message. + public DateTimeOffset Date { + get { return date; } + set { + if (date == value) + return; + + SetHeader ("Date", DateUtils.FormatDate (value)); + date = value; + } + } + + /// + /// Get or set the Resent-Date of the message. + /// + /// + /// Gets or sets the Resent-Date of the message. + /// + /// The Resent-Date of the message. + public DateTimeOffset ResentDate { + get { return resentDate; } + set { + if (resentDate == value) + return; + + SetHeader ("Resent-Date", DateUtils.FormatDate (value)); + resentDate = value; + } + } + + /// + /// Get the list of references to other messages. + /// + /// + /// The References header contains a chain of Message-Ids back to the + /// original message that started the thread. + /// + /// The references. + public MessageIdList References { + get { return references; } + } + + /// + /// Get or set the Message-Id that this message is replying to. + /// + /// + /// If the message is a reply to another message, it will typically + /// use the In-Reply-To header to specify the Message-Id of the + /// original message being replied to. + /// + /// The message id that this message is in reply to. + /// + /// is improperly formatted. + /// + public string InReplyTo { + get { return inreplyto; } + set { + if (inreplyto == value) + return; + + if (value == null) { + RemoveHeader (HeaderId.InReplyTo); + inreplyto = null; + return; + } + + var buffer = Encoding.UTF8.GetBytes (value); + MailboxAddress mailbox; + int index = 0; + + if (!MailboxAddress.TryParse (Headers.Options, buffer, ref index, buffer.Length, false, out mailbox)) + throw new ArgumentException ("Invalid Message-Id format.", nameof (value)); + + inreplyto = mailbox.Address; + + SetHeader ("In-Reply-To", "<" + inreplyto + ">"); + } + } + + /// + /// Get or set the message identifier. + /// + /// + /// The Message-Id is meant to be a globally unique identifier for + /// a message. + /// can be used + /// to generate this value. + /// + /// The message identifier. + /// + /// is null. + /// + /// + /// is improperly formatted. + /// + public string MessageId { + get { return messageId; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (messageId == value) + return; + + var buffer = Encoding.UTF8.GetBytes (value); + MailboxAddress mailbox; + int index = 0; + + if (!MailboxAddress.TryParse (Headers.Options, buffer, ref index, buffer.Length, false, out mailbox)) + throw new ArgumentException ("Invalid Message-Id format.", nameof (value)); + + messageId = mailbox.Address; + + SetHeader ("Message-Id", "<" + messageId + ">"); + } + } + + /// + /// Get or set the Resent-Message-Id header. + /// + /// + /// The Resent-Message-Id is meant to be a globally unique identifier for + /// a message. + /// can be used + /// to generate this value. + /// + /// The Resent-Message-Id. + /// + /// is null. + /// + /// + /// is improperly formatted. + /// + public string ResentMessageId { + get { return resentMessageId; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (resentMessageId == value) + return; + + var buffer = Encoding.UTF8.GetBytes (value); + MailboxAddress mailbox; + int index = 0; + + if (!MailboxAddress.TryParse (Headers.Options, buffer, ref index, buffer.Length, false, out mailbox)) + throw new ArgumentException ("Invalid Resent-Message-Id format.", nameof (value)); + + resentMessageId = mailbox.Address; + + SetHeader ("Resent-Message-Id", "<" + resentMessageId + ">"); + } + } + + /// + /// Get or set the MIME-Version. + /// + /// + /// The MIME-Version header specifies the version of the MIME specification + /// that the message was created for. + /// + /// The MIME version. + /// + /// is null. + /// + public Version MimeVersion { + get { return version; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (version != null && version.CompareTo (value) == 0) + return; + + SetHeader ("MIME-Version", value.ToString ()); + version = value; + } + } + + /// + /// Get or set the body of the message. + /// + /// + /// The body of the message can either be plain text or it can be a + /// tree of MIME entities such as a text/plain MIME part and a collection + /// of file attachments. + /// For a convenient way of constructing message bodies, see the + /// class. + /// + /// The body of the message. + public MimeEntity Body { + get; set; + } + + static bool TryGetMultipartBody (Multipart multipart, TextFormat format, out string body) + { + var alternative = multipart as MultipartAlternative; + + if (alternative != null) { + body = alternative.GetTextBody (format); + return body != null; + } + + var related = multipart as MultipartRelated; + Multipart multi; + TextPart text; + + if (related == null) { + // Note: This is probably a multipart/mixed... and if not, we can still treat it like it is. + for (int i = 0; i < multipart.Count; i++) { + multi = multipart[i] as Multipart; + + // descend into nested multiparts, if there are any... + if (multi != null) { + if (TryGetMultipartBody (multi, format, out body)) + return true; + + // The text body should never come after a multipart. + break; + } + + text = multipart[i] as TextPart; + + // Look for the first non-attachment text part (realistically, the body text will + // preceed any attachments, but I'm not sure we can rely on that assumption). + if (text != null && !text.IsAttachment) { + if (text.IsFormat (format)) { + body = MultipartAlternative.GetText (text); + return true; + } + + // Note: the first text/* part in a multipart/mixed is the text body. + // If it's not in the format we're looking for, then it doesn't exist. + break; + } + } + } else { + // Note: If the multipart/related root document is HTML, then this is the droid we are looking for. + var root = related.Root; + + text = root as TextPart; + + if (text != null) { + body = text.IsFormat (format) ? text.Text : null; + return body != null; + } + + // maybe the root is another multipart (like multipart/alternative)? + multi = root as Multipart; + + if (multi != null) + return TryGetMultipartBody (multi, format, out body); + } + + body = null; + + return false; + } + + /// + /// Get the text body of the message if it exists. + /// + /// + /// Gets the text content of the first text/plain body part that is found (in depth-first + /// search order) which is not an attachment. + /// + /// The text body if it exists; otherwise, null. + public string TextBody { + get { return GetTextBody (TextFormat.Plain); } + } + + /// + /// Get the html body of the message if it exists. + /// + /// + /// Gets the HTML-formatted body of the message if it exists. + /// + /// The html body if it exists; otherwise, null. + public string HtmlBody { + get { return GetTextBody (TextFormat.Html); } + } + + /// + /// Get the text body in the specified format. + /// + /// + /// Gets the text body in the specified format, if it exists. + /// + /// The text body in the desired format if it exists; otherwise, null. + /// The desired text format. + public string GetTextBody (TextFormat format) + { + var multipart = Body as Multipart; + + if (multipart != null) { + string text; + + if (TryGetMultipartBody (multipart, format, out text)) + return text; + } else { + var body = Body as TextPart; + + if (body != null && body.IsFormat (format) && !body.IsAttachment) + return body.Text; + } + + return null; + } + + static IEnumerable EnumerateMimeParts (MimeEntity entity) + { + if (entity == null) + yield break; + + var multipart = entity as Multipart; + + if (multipart != null) { + foreach (var subpart in multipart) { + foreach (var part in EnumerateMimeParts (subpart)) + yield return part; + } + + yield break; + } + + yield return entity; + } + + /// + /// Get the body parts of the message. + /// + /// + /// Traverses over the MIME tree, enumerating all of the objects, + /// but does not traverse into the bodies of attached messages. + /// + /// + /// + /// + /// The body parts. + public IEnumerable BodyParts { + get { return EnumerateMimeParts (Body); } + } + + /// + /// Get the attachments. + /// + /// + /// Traverses over the MIME tree, enumerating all of the objects that + /// have a Content-Disposition header set to "attachment". + /// + /// + /// + /// + /// The attachments. + public IEnumerable Attachments { + get { return EnumerateMimeParts (Body).Where (x => x.IsAttachment); } + } + + /// + /// Returns a that represents the for debugging purposes. + /// + /// + /// Returns a that represents the for debugging purposes. + /// In general, the string returned from this method SHOULD NOT be used for serializing + /// the message to disk. It is recommended that you use instead. + /// If this method is used for serializing the message to disk, the iso-8859-1 text encoding should be used for + /// conversion. + /// + /// A that represents the for debugging purposes. + public override string ToString () + { + using (var memory = new MemoryStream ()) { + WriteTo (FormatOptions.Default, memory); + +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var buffer = memory.GetBuffer (); +#else + var buffer = memory.ToArray (); +#endif + int count = (int) memory.Length; + + return CharsetUtils.Latin1.GetString (buffer, 0, count); + } + } + + /// + /// Dispatches to the specific visit method for this MIME message. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public virtual void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMimeMessage (this); + } + + /// + /// Prepare the message for transport using the specified encoding constraints. + /// + /// + /// Prepares the message for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum allowable length for a line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public virtual void Prepare (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + if (Body != null) { + if (MimeVersion == null && Body.Headers.Count > 0) + MimeVersion = new Version (1, 0); + + Body.Prepare (constraint, maxLineLength); + } + } + + /// + /// Write the message to the specified output stream. + /// + /// + /// Writes the message to the output stream using the provided formatting options. + /// + /// The formatting options. + /// The output stream. + /// true if only the headers should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, Stream stream, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (compliance == RfcComplianceMode.Strict && Body != null && Body.Headers.Count > 0 && !Headers.Contains (HeaderId.MimeVersion)) + MimeVersion = new Version (1, 0); + + if (Body != null) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + foreach (var header in MergeHeaders ()) { + if (options.HiddenHeaders.Contains (header.Id)) + continue; + + filtered.Write (header.RawField, 0, header.RawField.Length, cancellationToken); + + if (!header.IsInvalid) { + var rawValue = header.GetRawValue (options); + + filtered.Write (Header.Colon, 0, Header.Colon.Length, cancellationToken); + filtered.Write (rawValue, 0, rawValue.Length, cancellationToken); + } + } + + filtered.Flush (cancellationToken); + } + + var cancellable = stream as ICancellableStream; + + if (cancellable != null) { + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + + if (!headersOnly) { + try { + Body.EnsureNewLine = compliance == RfcComplianceMode.Strict || options.EnsureNewLine; + Body.WriteTo (options, stream, true, cancellationToken); + } finally { + Body.EnsureNewLine = false; + } + } + } else { + Headers.WriteTo (options, stream, cancellationToken); + } + } + + /// + /// Asynchronously write the message to the specified output stream. + /// + /// + /// Asynchronously writes the message to the output stream using the provided formatting options. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// true if only the headers should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (FormatOptions options, Stream stream, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + if (compliance == RfcComplianceMode.Strict && Body != null && Body.Headers.Count > 0 && !Headers.Contains (HeaderId.MimeVersion)) + MimeVersion = new Version (1, 0); + + if (Body != null) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (options.CreateNewLineFilter ()); + + foreach (var header in MergeHeaders ()) { + if (options.HiddenHeaders.Contains (header.Id)) + continue; + + await filtered.WriteAsync (header.RawField, 0, header.RawField.Length, cancellationToken).ConfigureAwait (false); + + if (!header.IsInvalid) { + var rawValue = header.GetRawValue (options); + + await filtered.WriteAsync (Header.Colon, 0, Header.Colon.Length, cancellationToken).ConfigureAwait (false); + await filtered.WriteAsync (rawValue, 0, rawValue.Length, cancellationToken).ConfigureAwait (false); + } + } + + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + + if (!headersOnly) { + try { + Body.EnsureNewLine = compliance == RfcComplianceMode.Strict || options.EnsureNewLine; + await Body.WriteToAsync (options, stream, true, cancellationToken).ConfigureAwait (false); + } finally { + Body.EnsureNewLine = false; + } + } + } else { + await Headers.WriteToAsync (options, stream, cancellationToken).ConfigureAwait (false); + } + } + + /// + /// Write the message to the specified output stream. + /// + /// + /// Writes the message to the output stream using the provided formatting options. + /// + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (options, stream, false, cancellationToken); + } + + /// + /// Asynchronously write the message to the specified output stream. + /// + /// + /// Asynchronously writes the message to the output stream using the provided formatting options. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (FormatOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (options, stream, false, cancellationToken); + } + + /// + /// Write the message to the specified output stream. + /// + /// + /// Writes the message to the output stream using the default formatting options. + /// + /// The output stream. + /// true if only the headers should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, stream, headersOnly, cancellationToken); + } + + /// + /// Asynchronously write the message to the specified output stream. + /// + /// + /// Asynchronously writes the message to the output stream using the default formatting options. + /// + /// An awaitable task. + /// The output stream. + /// true if only the headers should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (Stream stream, bool headersOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, stream, headersOnly, cancellationToken); + } + + /// + /// Write the message to the specified output stream. + /// + /// + /// Writes the message to the output stream using the default formatting options. + /// + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, stream, false, cancellationToken); + } + + /// + /// Asynchronously write the message to the specified output stream. + /// + /// + /// Asynchronously writes the message to the output stream using the default formatting options. + /// + /// An awaitable task. + /// The output stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, stream, false, cancellationToken); + } + + /// + /// Write the message to the specified file. + /// + /// + /// Writes the message to the specified file using the provided formatting options. + /// + /// The formatting options. + /// The file. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (FormatOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) { + WriteTo (options, stream, cancellationToken); + stream.Flush (); + } + } + + /// + /// Asynchronously write the message to the specified file. + /// + /// + /// Asynchronously writes the message to the specified file using the provided formatting options. + /// + /// An awaitable task. + /// The formatting options. + /// The file. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public async Task WriteToAsync (FormatOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.Open (fileName, FileMode.Create, FileAccess.Write)) { + await WriteToAsync (options, stream, cancellationToken).ConfigureAwait (false); + await stream.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + + /// + /// Write the message to the specified file. + /// + /// + /// Writes the message to the specified file using the default formatting options. + /// + /// The file. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public void WriteTo (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + WriteTo (FormatOptions.Default, fileName, cancellationToken); + } + + /// + /// Asynchronously write the message to the specified file. + /// + /// + /// Asynchronously writes the message to the specified file using the default formatting options. + /// + /// An awaitable task. + /// The file. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to write to the specified file. + /// + /// + /// An I/O error occurred. + /// + public Task WriteToAsync (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return WriteToAsync (FormatOptions.Default, fileName, cancellationToken); + } + + MailboxAddress GetMessageSigner () + { + if (ResentSender != null) + return ResentSender; + + if (ResentFrom.Count > 0) + return ResentFrom.Mailboxes.FirstOrDefault (); + + if (Sender != null) + return Sender; + + if (From.Count > 0) + return From.Mailboxes.FirstOrDefault (); + + return null; + } + + IList GetMessageRecipients (bool includeSenders) + { + var recipients = new HashSet (); + + if (ResentSender != null || ResentFrom.Count > 0) { + if (includeSenders) { + if (ResentSender != null) + recipients.Add (ResentSender); + + if (ResentFrom.Count > 0) { + foreach (var mailbox in ResentFrom.Mailboxes) + recipients.Add (mailbox); + } + } + + foreach (var mailbox in ResentTo.Mailboxes) + recipients.Add (mailbox); + foreach (var mailbox in ResentCc.Mailboxes) + recipients.Add (mailbox); + foreach (var mailbox in ResentBcc.Mailboxes) + recipients.Add (mailbox); + } else { + if (includeSenders) { + if (Sender != null) + recipients.Add (Sender); + + if (From.Count > 0) { + foreach (var mailbox in From.Mailboxes) + recipients.Add (mailbox); + } + } + + foreach (var mailbox in To.Mailboxes) + recipients.Add (mailbox); + foreach (var mailbox in Cc.Mailboxes) + recipients.Add (mailbox); + foreach (var mailbox in Bcc.Mailboxes) + recipients.Add (mailbox); + } + + return recipients.ToList (); + } + +#if ENABLE_CRYPTO + internal byte[] HashBody (FormatOptions options, DkimSignatureAlgorithm signatureAlgorithm, DkimCanonicalizationAlgorithm bodyCanonicalizationAlgorithm, int maxLength) + { + using (var stream = new DkimHashStream (signatureAlgorithm, maxLength)) { + using (var filtered = new FilteredStream (stream)) { + DkimBodyFilter dkim; + + if (bodyCanonicalizationAlgorithm == DkimCanonicalizationAlgorithm.Relaxed) + dkim = new DkimRelaxedBodyFilter (); + else + dkim = new DkimSimpleBodyFilter (); + + filtered.Add (options.CreateNewLineFilter ()); + filtered.Add (dkim); + + if (Body != null) { + try { + Body.EnsureNewLine = compliance == RfcComplianceMode.Strict || options.EnsureNewLine; + Body.WriteTo (options, filtered, true, CancellationToken.None); + } finally { + Body.EnsureNewLine = false; + } + } + + filtered.Flush (); + + if (!dkim.LastWasNewLine) + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + + return stream.GenerateHash (); + } + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The formatting options. + /// The DKIM signer. + /// The list of header fields to sign. + /// The header canonicalization algorithm. + /// The body canonicalization algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + [Obsolete ("Use DkimSigner.Sign() instead.")] + public void Sign (FormatOptions options, DkimSigner signer, IList headers, DkimCanonicalizationAlgorithm headerCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, DkimCanonicalizationAlgorithm bodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + signer.HeaderCanonicalizationAlgorithm = headerCanonicalizationAlgorithm; + signer.BodyCanonicalizationAlgorithm = bodyCanonicalizationAlgorithm; + + signer.Sign (options, this, headers); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The DKIM signer. + /// The headers to sign. + /// The header canonicalization algorithm. + /// The body canonicalization algorithm. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + [Obsolete ("Use DkimSigner.Sign() instead.")] + public void Sign (DkimSigner signer, IList headers, DkimCanonicalizationAlgorithm headerCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, DkimCanonicalizationAlgorithm bodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple) + { + Sign (FormatOptions.Default, signer, headers, headerCanonicalizationAlgorithm, bodyCanonicalizationAlgorithm); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The formatting options. + /// The DKIM signer. + /// The list of header fields to sign. + /// The header canonicalization algorithm. + /// The body canonicalization algorithm. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + [Obsolete ("Use DkimSigner.Sign() instead.")] + public void Sign (FormatOptions options, DkimSigner signer, IList headers, DkimCanonicalizationAlgorithm headerCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, DkimCanonicalizationAlgorithm bodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + signer.HeaderCanonicalizationAlgorithm = headerCanonicalizationAlgorithm; + signer.BodyCanonicalizationAlgorithm = bodyCanonicalizationAlgorithm; + + signer.Sign (options, this, headers); + } + + /// + /// Digitally sign the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// Digitally signs the message using a DomainKeys Identified Mail (DKIM) signature. + /// + /// + /// + /// + /// The DKIM signer. + /// The headers to sign. + /// The header canonicalization algorithm. + /// The body canonicalization algorithm. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain the 'From' header. + /// -or- + /// contains one or more of the following headers: Return-Path, + /// Received, Comments, Keywords, Bcc, Resent-Bcc, or DKIM-Signature. + /// + [Obsolete ("Use DkimSigner.Sign() instead.")] + public void Sign (DkimSigner signer, IList headers, DkimCanonicalizationAlgorithm headerCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple, DkimCanonicalizationAlgorithm bodyCanonicalizationAlgorithm = DkimCanonicalizationAlgorithm.Simple) + { + Sign (FormatOptions.Default, signer, headers, headerCanonicalizationAlgorithm, bodyCanonicalizationAlgorithm); + } + + Task DkimVerifyAsync (FormatOptions options, Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, bool doAsync, CancellationToken cancellationToken) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (dkimSignature == null) + throw new ArgumentNullException (nameof (dkimSignature)); + + if (dkimSignature.Id != HeaderId.DkimSignature) + throw new ArgumentException ("The signature parameter MUST be a DKIM-Signature header.", nameof (dkimSignature)); + + var verifier = new DkimVerifier (publicKeyLocator); + + if (doAsync) + return verifier.VerifyAsync (options, this, dkimSignature, cancellationToken); + + return Task.FromResult (verifier.Verify (options, this, dkimSignature, cancellationToken)); + } + + /// + /// Verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The formatting options. + /// The DKIM-Signature header. + /// The public key locator service. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + [Obsolete ("Use the DkimVerifier class instead.")] + public bool Verify (FormatOptions options, Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, CancellationToken cancellationToken = default (CancellationToken)) + { + return DkimVerifyAsync (options, dkimSignature, publicKeyLocator, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The formatting options. + /// The DKIM-Signature header. + /// The public key locator service. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + [Obsolete ("Use the DkimVerifier class instead.")] + public Task VerifyAsync (FormatOptions options, Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, CancellationToken cancellationToken = default (CancellationToken)) + { + return DkimVerifyAsync (options, dkimSignature, publicKeyLocator, true, cancellationToken); + } + + /// + /// Verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The DKIM-Signature header. + /// The public key locator service. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + [Obsolete ("Use the DkimVerifier class instead.")] + public bool Verify (Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, CancellationToken cancellationToken = default (CancellationToken)) + { + return Verify (FormatOptions.Default, dkimSignature, publicKeyLocator, cancellationToken); + } + + /// + /// Asynchronously verify the specified DKIM-Signature header. + /// + /// + /// Verifies the specified DKIM-Signature header. + /// + /// + /// + /// + /// true if the DKIM-Signature is valid; otherwise, false. + /// The DKIM-Signature header. + /// The public key locator service. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a DKIM-Signature header. + /// + /// + /// The DKIM-Signature header value is malformed. + /// + /// + /// The operation was canceled via the cancellation token. + /// + [Obsolete ("Use the DkimVerifier class instead.")] + public Task VerifyAsync (Header dkimSignature, IDkimPublicKeyLocator publicKeyLocator, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (FormatOptions.Default, dkimSignature, publicKeyLocator, cancellationToken); + } + + /// + /// Sign the message using the specified cryptography context and digest algorithm. + /// + /// + /// If either of the Resent-Sender or Resent-From headers are set, then the message + /// will be signed using the Resent-Sender (or first mailbox in the Resent-From) + /// address as the signer address, otherwise the Sender or From address will be + /// used instead. + /// + /// The cryptography context. + /// The digest algorithm. + /// + /// is null. + /// + /// + /// The has not been set. + /// -or- + /// A sender has not been specified. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// A signing certificate could not be found for the sender. + /// + /// + /// The private key could not be found for the sender. + /// + public void Sign (CryptographyContext ctx, DigestAlgorithm digestAlgo) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (Body == null) + throw new InvalidOperationException ("No message body has been set."); + + var signer = GetMessageSigner (); + if (signer == null) + throw new InvalidOperationException ("The sender has not been set."); + + Body = MultipartSigned.Create (ctx, signer, digestAlgo, Body); + } + + /// + /// Sign the message using the specified cryptography context and the SHA-1 digest algorithm. + /// + /// + /// If either of the Resent-Sender or Resent-From headers are set, then the message + /// will be signed using the Resent-Sender (or first mailbox in the Resent-From) + /// address as the signer address, otherwise the Sender or From address will be + /// used instead. + /// + /// The cryptography context. + /// + /// is null. + /// + /// + /// The has not been set. + /// -or- + /// A sender has not been specified. + /// + /// + /// A signing certificate could not be found for the sender. + /// + /// + /// The private key could not be found for the sender. + /// + public void Sign (CryptographyContext ctx) + { + Sign (ctx, DigestAlgorithm.Sha1); + } + + /// + /// Encrypt the message to the sender and all of the recipients + /// using the specified cryptography context. + /// + /// + /// If either of the Resent-Sender or Resent-From headers are set, then the message + /// will be encrypted to all of the addresses specified in the Resent headers + /// (Resent-Sender, Resent-From, Resent-To, Resent-Cc, and Resent-Bcc), + /// otherwise the message will be encrypted to all of the addresses specified in + /// the standard address headers (Sender, From, To, Cc, and Bcc). + /// + /// The cryptography context. + /// + /// is null. + /// + /// + /// An unknown type of cryptography context was used. + /// + /// + /// The has not been set. + /// -or- + /// No recipients have been specified. + /// + /// + /// A certificate could not be found for one or more of the recipients. + /// + /// + /// The public key could not be found for one or more of the recipients. + /// + public void Encrypt (CryptographyContext ctx) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (Body == null) + throw new InvalidOperationException ("No message body has been set."); + + var recipients = GetMessageRecipients (true); + if (recipients.Count == 0) + throw new InvalidOperationException ("No recipients have been set."); + + if (ctx is SecureMimeContext) { + Body = ApplicationPkcs7Mime.Encrypt ((SecureMimeContext) ctx, recipients, Body); + } else if (ctx is OpenPgpContext) { + Body = MultipartEncrypted.Encrypt ((OpenPgpContext) ctx, recipients, Body); + } else { + throw new ArgumentException ("Unknown type of cryptography context.", nameof (ctx)); + } + } + + /// + /// Sign and encrypt the message to the sender and all of the recipients using + /// the specified cryptography context and the specified digest algorithm. + /// + /// + /// If either of the Resent-Sender or Resent-From headers are set, then the message + /// will be signed using the Resent-Sender (or first mailbox in the Resent-From) + /// address as the signer address, otherwise the Sender or From address will be + /// used instead. + /// Likewise, if either of the Resent-Sender or Resent-From headers are set, then the + /// message will be encrypted to all of the addresses specified in the Resent headers + /// (Resent-Sender, Resent-From, Resent-To, Resent-Cc, and Resent-Bcc), + /// otherwise the message will be encrypted to all of the addresses specified in + /// the standard address headers (Sender, From, To, Cc, and Bcc). + /// + /// The cryptography context. + /// The digest algorithm. + /// + /// is null. + /// + /// + /// An unknown type of cryptography context was used. + /// + /// + /// The was out of range. + /// + /// + /// The has not been set. + /// -or- + /// No sender has been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// The is not supported. + /// + /// + /// A certificate could not be found for the signer or one or more of the recipients. + /// + /// + /// The private key could not be found for the sender. + /// + /// + /// The public key could not be found for one or more of the recipients. + /// + public void SignAndEncrypt (CryptographyContext ctx, DigestAlgorithm digestAlgo) + { + if (ctx == null) + throw new ArgumentNullException (nameof (ctx)); + + if (Body == null) + throw new InvalidOperationException ("No message body has been set."); + + var signer = GetMessageSigner (); + if (signer == null) + throw new InvalidOperationException ("The sender has not been set."); + + var recipients = GetMessageRecipients (true); + + if (ctx is SecureMimeContext) { + Body = ApplicationPkcs7Mime.SignAndEncrypt ((SecureMimeContext) ctx, signer, digestAlgo, recipients, Body); + } else if (ctx is OpenPgpContext) { + Body = MultipartEncrypted.SignAndEncrypt ((OpenPgpContext) ctx, signer, digestAlgo, recipients, Body); + } else { + throw new ArgumentException ("Unknown type of cryptography context.", nameof (ctx)); + } + } + + /// + /// Sign and encrypt the message to the sender and all of the recipients using + /// the specified cryptography context and the SHA-1 digest algorithm. + /// + /// + /// If either of the Resent-Sender or Resent-From headers are set, then the message + /// will be signed using the Resent-Sender (or first mailbox in the Resent-From) + /// address as the signer address, otherwise the Sender or From address will be + /// used instead. + /// Likewise, if either of the Resent-Sender or Resent-From headers are set, then the + /// message will be encrypted to all of the addresses specified in the Resent headers + /// (Resent-Sender, Resent-From, Resent-To, Resent-Cc, and Resent-Bcc), + /// otherwise the message will be encrypted to all of the addresses specified in + /// the standard address headers (Sender, From, To, Cc, and Bcc). + /// + /// The cryptography context. + /// + /// is null. + /// + /// + /// An unknown type of cryptography context was used. + /// + /// + /// The has not been set. + /// -or- + /// No sender has been specified. + /// -or- + /// No recipients have been specified. + /// + /// + /// A certificate could not be found for the signer or one or more of the recipients. + /// + /// + /// The private key could not be found for the sender. + /// + /// + /// The public key could not be found for one or more of the recipients. + /// + public void SignAndEncrypt (CryptographyContext ctx) + { + SignAndEncrypt (ctx, DigestAlgorithm.Sha1); + } +#endif // ENABLE_CRYPTO + + IEnumerable
MergeHeaders () + { + int mesgIndex = 0, bodyIndex = 0; + + // write all of the prepended message headers first + while (mesgIndex < Headers.Count) { + var mesgHeader = Headers[mesgIndex]; + if (mesgHeader.Offset.HasValue) + break; + + yield return mesgHeader; + mesgIndex++; + } + + // now merge the message and body headers as they appeared in the raw message + while (mesgIndex < Headers.Count && bodyIndex < Body.Headers.Count) { + var bodyHeader = Body.Headers[bodyIndex]; + if (!bodyHeader.Offset.HasValue) + break; + + var mesgHeader = Headers[mesgIndex]; + + if (mesgHeader.Offset.HasValue && mesgHeader.Offset < bodyHeader.Offset) { + yield return mesgHeader; + + mesgIndex++; + } else { + yield return bodyHeader; + + bodyIndex++; + } + } + + while (mesgIndex < Headers.Count) + yield return Headers[mesgIndex++]; + + while (bodyIndex < Body.Headers.Count) + yield return Body.Headers[bodyIndex++]; + } + + void RemoveHeader (HeaderId id) + { + Headers.Changed -= HeadersChanged; + + try { + Headers.RemoveAll (id); + } finally { + Headers.Changed += HeadersChanged; + } + } + + void ReplaceHeader (HeaderId id, string name, byte[] raw) + { + Headers.Changed -= HeadersChanged; + + try { + Headers.Replace (new Header (Headers.Options, id, name, raw)); + } finally { + Headers.Changed += HeadersChanged; + } + } + + void SetHeader (string name, string value) + { + Headers.Changed -= HeadersChanged; + + try { + Headers[name] = value; + } finally { + Headers.Changed += HeadersChanged; + } + } + + void SerializeAddressList (string field, InternetAddressList list) + { + if (list.Count == 0) { + RemoveHeader (field.ToHeaderId ()); + return; + } + + var builder = new StringBuilder (" "); + var options = FormatOptions.Default; + int lineLength = field.Length + 2; + + list.Encode (options, builder, true, ref lineLength); + builder.Append (options.NewLine); + + var raw = Encoding.UTF8.GetBytes (builder.ToString ()); + + ReplaceHeader (field.ToHeaderId (), field, raw); + } + + void InternetAddressListChanged (object addrlist, EventArgs e) + { + var list = (InternetAddressList) addrlist; + + foreach (var name in StandardAddressHeaders) { + if (addresses[name] == list) { + SerializeAddressList (name, list); + break; + } + } + } + + void ReferencesChanged (object o, EventArgs e) + { + if (references.Count > 0) { + int lineLength = "References".Length + 1; + var options = FormatOptions.Default; + var builder = new StringBuilder (); + + for (int i = 0; i < references.Count; i++) { + if (i > 0 && lineLength + references[i].Length + 2 >= options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + lineLength += references[i].Length; + builder.Append ("<" + references[i] + ">"); + } + + builder.Append (options.NewLine); + + var raw = Encoding.UTF8.GetBytes (builder.ToString ()); + + ReplaceHeader (HeaderId.References, "References", raw); + } else { + RemoveHeader (HeaderId.References); + } + } + + void AddAddresses (Header header, InternetAddressList list) + { + int length = header.RawValue.Length; + List parsed; + int index = 0; + + // parse the addresses in the new header and add them to our address list + if (!InternetAddressList.TryParse (Headers.Options, header.RawValue, ref index, length, false, 0, false, out parsed)) + return; + + list.Changed -= InternetAddressListChanged; + list.AddRange (parsed); + list.Changed += InternetAddressListChanged; + } + + void ReloadAddressList (HeaderId id, InternetAddressList list) + { + // clear the address list and reload + list.Changed -= InternetAddressListChanged; + list.Clear (); + + foreach (var header in Headers) { + if (header.Id != id) + continue; + + int length = header.RawValue.Length; + List parsed; + int index = 0; + + if (!InternetAddressList.TryParse (Headers.Options, header.RawValue, ref index, length, false, 0, false, out parsed)) + continue; + + list.AddRange (parsed); + } + + list.Changed += InternetAddressListChanged; + } + + void ReloadHeader (HeaderId id) + { + if (id == HeaderId.Unknown) + return; + + switch (id) { + case HeaderId.ResentMessageId: + resentMessageId = null; + break; + case HeaderId.ResentSender: + resentSender = null; + break; + case HeaderId.ResentDate: + resentDate = DateTimeOffset.MinValue; + break; + case HeaderId.References: + references.Changed -= ReferencesChanged; + references.Clear (); + references.Changed += ReferencesChanged; + break; + case HeaderId.InReplyTo: + inreplyto = null; + break; + case HeaderId.MessageId: + messageId = null; + break; + case HeaderId.Sender: + sender = null; + break; + case HeaderId.Importance: + importance = MessageImportance.Normal; + break; + case HeaderId.XPriority: + xpriority = XMessagePriority.Normal; + break; + case HeaderId.Priority: + priority = MessagePriority.Normal; + break; + case HeaderId.Date: + date = DateTimeOffset.MinValue; + break; + } + + foreach (var header in Headers) { + if (header.Id != id) + continue; + + var rawValue = header.RawValue; + int number, index = 0; + + switch (id) { + case HeaderId.MimeVersion: + MimeUtils.TryParse (rawValue, 0, rawValue.Length, out version); + break; + case HeaderId.References: + references.Changed -= ReferencesChanged; + foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) + references.Add (msgid); + references.Changed += ReferencesChanged; + break; + case HeaderId.InReplyTo: + inreplyto = MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length).FirstOrDefault (); + break; + case HeaderId.ResentMessageId: + resentMessageId = MimeUtils.ParseMessageId (rawValue, 0, rawValue.Length); + break; + case HeaderId.MessageId: + messageId = MimeUtils.ParseMessageId (rawValue, 0, rawValue.Length); + break; + case HeaderId.ResentSender: + MailboxAddress.TryParse (Headers.Options, rawValue, ref index, rawValue.Length, false, out resentSender); + break; + case HeaderId.Sender: + MailboxAddress.TryParse (Headers.Options, rawValue, ref index, rawValue.Length, false, out sender); + break; + case HeaderId.ResentDate: + DateUtils.TryParse (rawValue, 0, rawValue.Length, out resentDate); + break; + case HeaderId.Importance: + switch (header.Value.ToLowerInvariant ().Trim ()) { + case "high": importance = MessageImportance.High; break; + case "low": importance = MessageImportance.Low; break; + default: importance = MessageImportance.Normal; break; + } + break; + case HeaderId.Priority: + switch (header.Value.ToLowerInvariant ().Trim ()) { + case "non-urgent": priority = MessagePriority.NonUrgent; break; + case "urgent": priority = MessagePriority.Urgent; break; + default: priority = MessagePriority.Normal; break; + } + break; + case HeaderId.XPriority: + ParseUtils.SkipWhiteSpace (rawValue, ref index, rawValue.Length); + + if (ParseUtils.TryParseInt32 (rawValue, ref index, rawValue.Length, out number)) { + xpriority = (XMessagePriority) Math.Min (Math.Max (number, 1), 5); + } else { + xpriority = XMessagePriority.Normal; + } + break; + case HeaderId.Date: + DateUtils.TryParse (rawValue, 0, rawValue.Length, out date); + break; + } + } + } + + void HeadersChanged (object o, HeaderListChangedEventArgs e) + { + InternetAddressList list; + byte[] rawValue; + int index = 0; + int number; + + switch (e.Action) { + case HeaderListChangedAction.Added: + if (addresses.TryGetValue (e.Header.Field, out list)) { + AddAddresses (e.Header, list); + break; + } + + rawValue = e.Header.RawValue; + + switch (e.Header.Id) { + case HeaderId.MimeVersion: + MimeUtils.TryParse (rawValue, 0, rawValue.Length, out version); + break; + case HeaderId.References: + references.Changed -= ReferencesChanged; + foreach (var msgid in MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length)) + references.Add (msgid); + references.Changed += ReferencesChanged; + break; + case HeaderId.InReplyTo: + inreplyto = MimeUtils.EnumerateReferences (rawValue, 0, rawValue.Length).FirstOrDefault (); + break; + case HeaderId.ResentMessageId: + resentMessageId = MimeUtils.ParseMessageId (rawValue, 0, rawValue.Length); + break; + case HeaderId.MessageId: + messageId = MimeUtils.ParseMessageId (rawValue, 0, rawValue.Length); + break; + case HeaderId.ResentSender: + MailboxAddress.TryParse (Headers.Options, rawValue, ref index, rawValue.Length, false, out resentSender); + break; + case HeaderId.Sender: + MailboxAddress.TryParse (Headers.Options, rawValue, ref index, rawValue.Length, false, out sender); + break; + case HeaderId.ResentDate: + DateUtils.TryParse (rawValue, 0, rawValue.Length, out resentDate); + break; + case HeaderId.Importance: + switch (e.Header.Value.ToLowerInvariant ().Trim ()) { + case "high": importance = MessageImportance.High; break; + case "low": importance = MessageImportance.Low; break; + default: importance = MessageImportance.Normal; break; + } + break; + case HeaderId.Priority: + switch (e.Header.Value.ToLowerInvariant ().Trim ()) { + case "non-urgent": priority = MessagePriority.NonUrgent; break; + case "urgent": priority = MessagePriority.Urgent; break; + default: priority = MessagePriority.Normal; break; + } + break; + case HeaderId.XPriority: + ParseUtils.SkipWhiteSpace (rawValue, ref index, rawValue.Length); + + if (ParseUtils.TryParseInt32 (rawValue, ref index, rawValue.Length, out number)) { + xpriority = (XMessagePriority) Math.Min (Math.Max (number, 1), 5); + } else { + xpriority = XMessagePriority.Normal; + } + break; + case HeaderId.Date: + DateUtils.TryParse (rawValue, 0, rawValue.Length, out date); + break; + } + break; + case HeaderListChangedAction.Changed: + case HeaderListChangedAction.Removed: + if (addresses.TryGetValue (e.Header.Field, out list)) { + ReloadAddressList (e.Header.Id, list); + break; + } + + ReloadHeader (e.Header.Id); + break; + case HeaderListChangedAction.Cleared: + foreach (var kvp in addresses) { + kvp.Value.Changed -= InternetAddressListChanged; + kvp.Value.Clear (); + kvp.Value.Changed += InternetAddressListChanged; + } + + references.Changed -= ReferencesChanged; + references.Clear (); + references.Changed += ReferencesChanged; + + resentDate = date = DateTimeOffset.MinValue; + importance = MessageImportance.Normal; + xpriority = XMessagePriority.Normal; + priority = MessagePriority.Normal; + resentMessageId = null; + resentSender = null; + inreplyto = null; + messageId = null; + version = null; + sender = null; + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed message. + /// The parser options. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (ParserOptions options, Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity, persistent); + + return parser.ParseMessage (cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed message. + /// The parser options. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + var parser = new MimeParser (options, stream, MimeFormat.Entity, persistent); + + return parser.ParseMessageAsync (cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed message. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (options, stream, false, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// specified . + /// + /// The parsed message. + /// The parser options. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (ParserOptions options, Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (options, stream, false, cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed message. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, stream, persistent, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save mmeory usage, but also improve + /// performance. + /// + /// The parsed message. + /// The stream. + /// true if the stream is persistent; otherwise false. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (Stream stream, bool persistent, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, stream, persistent, cancellationToken); + } + + /// + /// Load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed message. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, stream, false, cancellationToken); + } + + /// + /// Asynchronously load a from the specified stream. + /// + /// + /// Loads a from the given stream, using the + /// default . + /// + /// The parsed message. + /// The stream. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (Stream stream, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, stream, false, cancellationToken); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the given path, using the + /// specified . + /// + /// The parsed message. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return Load (options, stream, cancellationToken); + } + + /// + /// Asynchronously load a from the specified file. + /// + /// + /// Loads a from the file at the given path, using the + /// specified . + /// + /// The parsed message. + /// The parser options. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static async Task LoadAsync (ParserOptions options, string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + using (var stream = File.OpenRead (fileName)) + return await LoadAsync (options, stream, cancellationToken).ConfigureAwait (false); + } + + /// + /// Load a from the specified file. + /// + /// + /// Loads a from the file at the given path, using the + /// default . + /// + /// The parsed message. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static MimeMessage Load (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return Load (ParserOptions.Default, fileName, cancellationToken); + } + + /// + /// Asynchronously load a from the specified file. + /// + /// + /// Loads a from the file at the given path, using the + /// default . + /// + /// The parsed message. + /// The name of the file to load. + /// The cancellation token. + /// + /// is null. + /// + /// + /// is a zero-length string, contains only white space, or + /// contains one or more invalid characters. + /// + /// + /// is an invalid file path. + /// + /// + /// The specified file path could not be found. + /// + /// + /// The user does not have access to read the specified file. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public static Task LoadAsync (string fileName, CancellationToken cancellationToken = default (CancellationToken)) + { + return LoadAsync (ParserOptions.Default, fileName, cancellationToken); + } + +#if ENABLE_SNM + static MimePart GetMimePart (AttachmentBase item) + { + var mimeType = item.ContentType.ToString (); + var contentType = ContentType.Parse (mimeType); + var attachment = item as Attachment; + MimePart part; + + if (contentType.MediaType.Equals ("text", StringComparison.OrdinalIgnoreCase)) + part = new TextPart (contentType); + else + part = new MimePart (contentType); + + if (attachment != null) { + var disposition = attachment.ContentDisposition.ToString (); + part.ContentDisposition = ContentDisposition.Parse (disposition); + } + + switch (item.TransferEncoding) { + case System.Net.Mime.TransferEncoding.QuotedPrintable: + part.ContentTransferEncoding = ContentEncoding.QuotedPrintable; + break; + case System.Net.Mime.TransferEncoding.Base64: + part.ContentTransferEncoding = ContentEncoding.Base64; + break; + case System.Net.Mime.TransferEncoding.SevenBit: + part.ContentTransferEncoding = ContentEncoding.SevenBit; + break; + //case System.Net.Mime.TransferEncoding.EightBit: + // part.ContentTransferEncoding = ContentEncoding.EightBit; + // break; + } + + if (item.ContentId != null) + part.ContentId = item.ContentId; + + var stream = new MemoryBlockStream (); + if (item.ContentStream.CanSeek) + item.ContentStream.Position = 0; + item.ContentStream.CopyTo (stream); + stream.Position = 0; + + part.Content = new MimeContent (stream); + + return part; + } + + /// + /// Creates a new from a . + /// + /// + /// Creates a new from a . + /// + /// The equivalent . + /// The message. + /// + /// is null. + /// + public static MimeMessage CreateFromMailMessage (MailMessage message) + { + if (message == null) + throw new ArgumentNullException (nameof (message)); + + var headers = new List
(); + foreach (var field in message.Headers.AllKeys) { + foreach (var value in message.Headers.GetValues (field)) + headers.Add (new Header (field, value)); + } + + var msg = new MimeMessage (ParserOptions.Default, headers, RfcComplianceMode.Strict); + MimeEntity body = null; + + // Note: If the user has already sent their MailMessage via System.Net.Mail.SmtpClient, + // then the following MailMessage properties will have been merged into the Headers, so + // check to make sure our MimeMessage properties are empty before adding them. + if (message.Sender != null) + msg.Sender = (MailboxAddress) message.Sender; + + if (message.From != null) { + msg.Headers.Replace (HeaderId.From, string.Empty); + msg.From.Add ((MailboxAddress) message.From); + } + + if (message.ReplyToList.Count > 0) { + msg.Headers.Replace (HeaderId.ReplyTo, string.Empty); + msg.ReplyTo.AddRange ((InternetAddressList) message.ReplyToList); + } + + if (message.To.Count > 0) { + msg.Headers.Replace (HeaderId.To, string.Empty); + msg.To.AddRange ((InternetAddressList) message.To); + } + + if (message.CC.Count > 0) { + msg.Headers.Replace (HeaderId.Cc, string.Empty); + msg.Cc.AddRange ((InternetAddressList) message.CC); + } + + if (message.Bcc.Count > 0) { + msg.Headers.Replace (HeaderId.Bcc, string.Empty); + msg.Bcc.AddRange ((InternetAddressList) message.Bcc); + } + + if (message.SubjectEncoding != null) + msg.Headers.Replace (HeaderId.Subject, message.SubjectEncoding, message.Subject ?? string.Empty); + else + msg.Subject = message.Subject ?? string.Empty; + + if (!msg.Headers.Contains (HeaderId.Date)) + msg.Date = DateTimeOffset.Now; + + switch (message.Priority) { + case MailPriority.Normal: + msg.Headers.RemoveAll (HeaderId.XMSMailPriority); + msg.Headers.RemoveAll (HeaderId.Importance); + msg.Headers.RemoveAll (HeaderId.XPriority); + msg.Headers.RemoveAll (HeaderId.Priority); + break; + case MailPriority.High: + msg.Headers.Replace (HeaderId.Priority, "urgent"); + msg.Headers.Replace (HeaderId.Importance, "high"); + msg.Headers.Replace (HeaderId.XPriority, "2 (High)"); + break; + case MailPriority.Low: + msg.Headers.Replace (HeaderId.Priority, "non-urgent"); + msg.Headers.Replace (HeaderId.Importance, "low"); + msg.Headers.Replace (HeaderId.XPriority, "4 (Low)"); + break; + } + + if (!string.IsNullOrEmpty (message.Body)) { + var text = new TextPart (message.IsBodyHtml ? "html" : "plain"); + text.SetText (message.BodyEncoding ?? Encoding.UTF8, message.Body); + body = text; + } + + if (message.AlternateViews.Count > 0) { + var alternative = new MultipartAlternative (); + + if (body != null) + alternative.Add (body); + + foreach (var view in message.AlternateViews) { + var part = GetMimePart (view); + + if (view.LinkedResources.Count > 0) { + var type = part.ContentType.MediaType + "/" + part.ContentType.MediaSubtype; + var related = new MultipartRelated (); + + related.ContentType.Parameters.Add ("type", type); + related.ContentBase = view.BaseUri; + + related.Add (part); + + foreach (var resource in view.LinkedResources) { + part = GetMimePart (resource); + + if (resource.ContentLink != null) + part.ContentLocation = resource.ContentLink; + + related.Add (part); + } + + alternative.Add (related); + } else { + part.ContentBase = view.BaseUri; + alternative.Add (part); + } + } + + body = alternative; + } + + if (body == null) + body = new TextPart (message.IsBodyHtml ? "html" : "plain"); + + if (message.Attachments.Count > 0) { + var mixed = new Multipart ("mixed"); + + if (body != null) + mixed.Add (body); + + foreach (var attachment in message.Attachments) + mixed.Add (GetMimePart (attachment)); + + body = mixed; + } + + msg.Body = body; + + return msg; + } + + /// + /// Explicit cast to convert a to a + /// . + /// + /// + /// Allows creation of messages using Microsoft's System.Net.Mail APIs. + /// + /// The equivalent . + /// The message. + public static explicit operator MimeMessage (MailMessage message) + { + return message != null ? CreateFromMailMessage (message) : null; + } +#endif + } +} diff --git a/src/MimeKit/MimeParser.cs b/src/MimeKit/MimeParser.cs new file mode 100644 index 0000000..e63d09e --- /dev/null +++ b/src/MimeKit/MimeParser.cs @@ -0,0 +1,2028 @@ +// +// MimeParser.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.IO; +using MimeKit.Utils; + +namespace MimeKit { + enum BoundaryType + { + None, + Eos, + ImmediateBoundary, + ImmediateEndBoundary, + ParentBoundary, + ParentEndBoundary, + } + + class Boundary + { + public static readonly byte[] MboxFrom = Encoding.ASCII.GetBytes ("From "); + + public byte[] Marker { get; private set; } + public int FinalLength { get { return Marker.Length; } } + public int Length { get; private set; } + public int MaxLength { get; private set; } + + public Boundary (string boundary, int currentMaxLength) + { + Marker = Encoding.UTF8.GetBytes ("--" + boundary + "--"); + Length = Marker.Length - 2; + + MaxLength = Math.Max (currentMaxLength, Marker.Length); + } + + Boundary () + { + } + + public static Boundary CreateMboxBoundary () + { + var boundary = new Boundary (); + boundary.Marker = MboxFrom; + boundary.MaxLength = 5; + boundary.Length = 5; + return boundary; + } + + public override string ToString () + { + return Encoding.UTF8.GetString (Marker, 0, Marker.Length); + } + } + + enum MimeParserState : sbyte + { + Error = -1, + Initialized, + MboxMarker, + MessageHeaders, + Headers, + Content, + Boundary, + Complete, + Eos + } + + /// + /// A MIME message and entity parser. + /// + /// + /// A MIME parser is used to parse and + /// objects from arbitrary streams. + /// + public partial class MimeParser : IEnumerable + { + static readonly byte[] UTF8ByteOrderMark = { 0xEF, 0xBB, 0xBF }; + const int ReadAheadSize = 128; + const int BlockSize = 4096; + const int PadSize = 4; + + // I/O buffering + readonly byte[] input = new byte[ReadAheadSize + BlockSize + PadSize]; + const int inputStart = ReadAheadSize; + int inputIndex = ReadAheadSize; + int inputEnd = ReadAheadSize; + + // mbox From-line state + byte[] mboxMarkerBuffer; + long mboxMarkerOffset; + int mboxMarkerLength; + + // message/rfc822 mbox markers (shouldn't exist, but sometimes do) + byte[] preHeaderBuffer = new byte[128]; + int preHeaderLength; + + // header buffer + byte[] headerBuffer = new byte[512]; + long headerOffset; + int headerIndex; + + readonly List bounds = new List (); + readonly List
headers = new List
(); + + MimeParserState state; + BoundaryType boundary; + MimeFormat format; + bool persistent; + bool toplevel; + bool eos; + + ParserOptions options; + long headerBlockBegin; + long headerBlockEnd; + long contentEnd; + int lineNumber; + Stream stream; + long position; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new that will parse the specified stream. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The stream to parse. + /// The format of the stream. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// + public MimeParser (Stream stream, MimeFormat format, bool persistent = false) : this (ParserOptions.Default, stream, format, persistent) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new that will parse the specified stream. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The stream to parse. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// + public MimeParser (Stream stream, bool persistent = false) : this (ParserOptions.Default, stream, MimeFormat.Default, persistent) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new that will parse the specified stream. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The parser options. + /// The stream to parse. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// -or- + /// is null. + /// + public MimeParser (ParserOptions options, Stream stream, bool persistent = false) : this (options, stream, MimeFormat.Default, persistent) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new that will parse the specified stream. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The parser options. + /// The stream to parse. + /// The format of the stream. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// -or- + /// is null. + /// + public MimeParser (ParserOptions options, Stream stream, MimeFormat format, bool persistent = false) + { + SetStream (options, stream, format, persistent); + } + + /// + /// Gets a value indicating whether the parser has reached the end of the input stream. + /// + /// + /// Gets a value indicating whether the parser has reached the end of the input stream. + /// + /// true if this parser has reached the end of the input stream; + /// otherwise, false. + public bool IsEndOfStream { + get { return state == MimeParserState.Eos; } + } + + /// + /// Gets the current position of the parser within the stream. + /// + /// + /// Gets the current position of the parser within the stream. + /// + /// The stream offset. + public long Position { + get { return GetOffset (-1); } + } + + /// + /// Gets the most recent mbox marker offset. + /// + /// + /// Gets the most recent mbox marker offset. + /// + /// The mbox marker offset. + public long MboxMarkerOffset { + get { return mboxMarkerOffset; } + } + + /// + /// Gets the most recent mbox marker. + /// + /// + /// Gets the most recent mbox marker. + /// + /// The mbox marker. + public string MboxMarker { + get { return Encoding.UTF8.GetString (mboxMarkerBuffer, 0, mboxMarkerLength); } + } + + /// + /// Sets the stream to parse. + /// + /// + /// Sets the stream to parse. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The parser options. + /// The stream to parse. + /// The format of the stream. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// -or- + /// is null. + /// + public void SetStream (ParserOptions options, Stream stream, MimeFormat format, bool persistent = false) + { + if (options == null) + throw new ArgumentNullException (nameof (options)); + + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + this.persistent = persistent && stream.CanSeek; + this.options = options.Clone (); + this.format = format; + this.stream = stream; + + inputIndex = inputStart; + inputEnd = inputStart; + + mboxMarkerOffset = 0; + mboxMarkerLength = 0; + headerBlockBegin = 0; + headerBlockEnd = 0; + lineNumber = 0; + contentEnd = 0; + + position = stream.CanSeek ? stream.Position : 0; + preHeaderLength = 0; + headers.Clear (); + headerOffset = 0; + headerIndex = 0; + toplevel = false; + eos = false; + + bounds.Clear (); + if (format == MimeFormat.Mbox) { + bounds.Add (Boundary.CreateMboxBoundary ()); + + if (mboxMarkerBuffer == null) + mboxMarkerBuffer = new byte[ReadAheadSize]; + } + + state = MimeParserState.Initialized; + boundary = BoundaryType.None; + } + + /// + /// Sets the stream to parse. + /// + /// + /// Sets the stream to parse. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The parser options. + /// The stream to parse. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// -or- + /// is null. + /// + public void SetStream (ParserOptions options, Stream stream, bool persistent = false) + { + SetStream (options, stream, MimeFormat.Default, persistent); + } + + /// + /// Sets the stream to parse. + /// + /// + /// Sets the stream to parse. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The stream to parse. + /// The format of the stream. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// + public void SetStream (Stream stream, MimeFormat format, bool persistent = false) + { + SetStream (ParserOptions.Default, stream, format, persistent); + } + + /// + /// Sets the stream to parse. + /// + /// + /// Sets the stream to parse. + /// If is true and is seekable, then + /// the will not copy the content of s into memory. Instead, + /// it will use a to reference a substream of . + /// This has the potential to not only save memory usage, but also improve + /// performance. + /// It should be noted, however, that disposing will make it impossible + /// for to read the content. + /// + /// The stream to parse. + /// true if the stream is persistent; otherwise false. + /// + /// is null. + /// + public void SetStream (Stream stream, bool persistent = false) + { + SetStream (ParserOptions.Default, stream, MimeFormat.Default, persistent); + } + + /// + /// Invoked when an mbox marker is found. + /// + /// + /// Invoked when an mbox marker is found, providing subclasses with the ability to track stream offsets. + /// + /// The stream offset at which the mbox marker begins. + protected virtual void OnMboxMarkerBegin (long offset) + { + } + + /// + /// Invoked when the end of an mbox marker is found. + /// + /// + /// Invoked when the end of an mbox marker is found, providing subclasses with the ability to track stream offsets. + /// + /// The stream offset at which the mbox marker ends. + protected virtual void OnMboxMarkerEnd (long offset) + { + } + + /// + /// Invoked when the beginning of a MIME message is found. + /// + /// + /// Invoked when the beginning of a MIME message is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME message. + /// The stream offset at which the MIME message begins. + protected virtual void OnMimeMessageBegin (MimeMessage message, long offset) + { + } + + /// + /// Invoked when the end of a MIME message is found. + /// + /// + /// Invoked when the end of a MIME message is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME message. + /// The stream offset at which the MIME message ends. + protected virtual void OnMimeMessageEnd (MimeMessage message, long offset) + { + } + + /// + /// Invoked when the end of the message headers is found. + /// + /// + /// Invoked when the end of the message headers is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME message. + /// The stream offset at which the MIME message headers end. + protected virtual void OnMimeMessageHeadersEnd (MimeMessage message, long offset) + { + } + + /// + /// Invoked when the beginning of a MIME entity is found. + /// + /// + /// Invoked when the beginning of a MIME entity is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME entity. + /// The stream offset at which the MIME entity begins. + protected virtual void OnMimeEntityBegin (MimeEntity entity, long offset) + { + } + + /// + /// Invoked when the end of a MIME entity is found. + /// + /// + /// Invoked when the end of a MIME entity is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME entity. + /// The stream offset at which the MIME entity ends. + protected virtual void OnMimeEntityEnd (MimeEntity entity, long offset) + { + } + + /// + /// Invoked when the end of MIME entity headers is found. + /// + /// + /// Invoked when the end of MIME entity headers is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME entity. + /// The stream offset at which the MIME entity ends. + protected virtual void OnMimeEntityHeadersEnd (MimeEntity entity, long offset) + { + } + + /// + /// Invoked when the beginning of a MIME entity's content is found. + /// + /// + /// Invoked when the beginning of a MIME entity's content is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME entity. + /// The stream offset at which the MIME entity's content begins. + protected virtual void OnMimeContentBegin (MimeEntity entity, long offset) + { + } + + /// + /// Invoked when the end of a MIME entity's content is found. + /// + /// + /// Invoked when the end of a MIME entity's content is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME entity. + /// The stream offset at which the MIME entity's content ends. + protected virtual void OnMimeContentEnd (MimeEntity entity, long offset) + { + } + + /// + /// Invoked when a multipart boundary is found. + /// + /// + /// Invoked when a multipart boundary is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the multipart boundary begins. + protected virtual void OnMultipartBoundaryBegin (Multipart multipart, long offset) + { + } + + /// + /// Invoked when the end of a multipart boundary is found. + /// + /// + /// Invoked when the end of a multipart boundary is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the multipart boundary ends. + protected virtual void OnMultipartBoundaryEnd (Multipart multipart, long offset) + { + } + + /// + /// Invoked when a multipart end-boundary is found. + /// + /// + /// Invoked when a multipart end-boundary is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the multipart end-boundary begins. + protected virtual void OnMultipartEndBoundaryBegin (Multipart multipart, long offset) + { + } + + /// + /// Invoked when the end of a multipart end-boundary is found. + /// + /// + /// Invoked when the end of a multipart end-boundary is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the multipart end-boundary ends. + protected virtual void OnMultipartEndBoundaryEnd (Multipart multipart, long offset) + { + } + + /// + /// Invoked when a multipart preamble is found. + /// + /// + /// Invoked when a multipart preamble is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the preamble begins. + protected virtual void OnMultipartPreambleBegin (Multipart multipart, long offset) + { + } + + /// + /// Invoked when the end of a multipart preamble is found. + /// + /// + /// Invoked when the end of a multipart preamble is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the preamble ends. + protected virtual void OnMultipartPreambleEnd (Multipart multipart, long offset) + { + } + + /// + /// Invoked when a multipart epilogue is found. + /// + /// + /// Invoked when a multipart epilogue is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the epilogue begins. + protected virtual void OnMultipartEpilogueBegin (Multipart multipart, long offset) + { + } + + /// + /// Invoked when the end of a multipart epilogue is found. + /// + /// + /// Invoked when the end of a multipart epilogue is found, providing subclasses with the ability to track stream offsets. + /// + /// The MIME multipart. + /// The stream offset at which the epilogue ends. + protected virtual void OnMultipartEpilogueEnd (Multipart multipart, long offset) + { + } + + /// + /// Invoked for all MIME entities once the octet count for the content has been calculated. + /// + /// + /// Invoked for all MIME entities once the octet count for the content has been calculated. + /// + /// The MIME entity. + /// The number of octets contained in the content of the entity. + protected virtual void OnMimeContentOctets (MimeEntity entity, long octets) + { + } + + /// + /// Invoked for all MIME entities once the line count for the content has been calculated. + /// + /// + /// Invoked for all MIME entities once the line count for the content has been calculated. + /// + /// The MIME entity. + /// The number of lines contained in the content of the entity. + protected virtual void OnMimeContentLines (MimeEntity entity, int lines) + { + } + +#if DEBUG + static string ConvertToCString (byte[] buffer, int startIndex, int length) + { + var cstr = new StringBuilder (); + cstr.AppendCString (buffer, startIndex, length); + return cstr.ToString (); + } +#endif + + static int NextAllocSize (int need) + { + return (need + 63) & ~63; + } + + bool AlignReadAheadBuffer (int atleast, int save, out int left, out int start, out int end) + { + left = inputEnd - inputIndex; + start = inputStart; + end = inputEnd; + + if (left >= atleast || eos) + return false; + + left += save; + + if (left > 0) { + int index = inputIndex - save; + + // attempt to align the end of the remaining input with ReadAheadSize + if (index >= start) { + start -= Math.Min (ReadAheadSize, left); + Buffer.BlockCopy (input, index, input, start, left); + index = start; + start += left; + } else if (index > 0) { + int shift = Math.Min (index, end - start); + Buffer.BlockCopy (input, index, input, index - shift, left); + index -= shift; + start = index + left; + } else { + // we can't shift... + start = end; + } + + inputIndex = index + save; + inputEnd = start; + } else { + inputIndex = start; + inputEnd = start; + } + + end = input.Length - PadSize; + + return true; + } + + int ReadAhead (int atleast, int save, CancellationToken cancellationToken) + { + int nread, left, start, end; + + if (!AlignReadAheadBuffer (atleast, save, out left, out start, out end)) + return left; + + // use the cancellable stream interface if available... + var cancellable = stream as ICancellableStream; + if (cancellable != null) { + nread = cancellable.Read (input, start, end - start, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + nread = stream.Read (input, start, end - start); + } + + if (nread > 0) { + inputEnd += nread; + position += nread; + } else { + eos = true; + } + + return inputEnd - inputIndex; + } + + long GetOffset (int index) + { + if (position == -1) + return -1; + + if (index == -1) + index = inputIndex; + + return position - (inputEnd - index); + } + + static unsafe bool CStringsEqual (byte* str1, byte* str2, int length) + { + byte* se = str1 + length; + byte* s1 = str1; + byte* s2 = str2; + + while (s1 < se) { + if (*s1++ != *s2++) + return false; + } + + return true; + } + + unsafe void StepByteOrderMark (byte* inbuf, ref int bomIndex) + { + byte* inptr = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + + while (inptr < inend && bomIndex < UTF8ByteOrderMark.Length && *inptr == UTF8ByteOrderMark[bomIndex]) { + bomIndex++; + inptr++; + } + + inputIndex = (int) (inptr - inbuf); + } + + unsafe bool StepByteOrderMark (byte* inbuf, CancellationToken cancellationToken) + { + int bomIndex = 0; + + do { + var available = ReadAhead (ReadAheadSize, 0, cancellationToken); + + if (available <= 0) { + // failed to read any data... EOF + inputIndex = inputEnd; + return false; + } + + StepByteOrderMark (inbuf, ref bomIndex); + } while (inputIndex == inputEnd); + + return bomIndex == 0 || bomIndex == UTF8ByteOrderMark.Length; + } + + static unsafe bool IsMboxMarker (byte* text, bool allowMunged = false) + { +#if COMPARE_QWORD + const ulong FromMask = 0x000000FFFFFFFFFF; + const ulong From = 0x000000206D6F7246; + ulong* qword = (ulong*) text; + + return (*qword & FromMask) == From; +#else + byte* inptr = text; + + if (allowMunged && *inptr == (byte) '>') + inptr++; + + return *inptr++ == (byte) 'F' && *inptr++ == (byte) 'r' && *inptr++ == (byte) 'o' && *inptr++ == (byte) 'm' && *inptr == (byte) ' '; +#endif + } + + unsafe void StepMboxMarker (byte *inbuf, ref bool needInput, ref bool complete, ref int left) + { + byte* inptr = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + + *inend = (byte) '\n'; + + while (inptr < inend) { + byte* start = inptr; + + // scan for the end of the line + while (*inptr != (byte) '\n') + inptr++; + + long length = inptr - start; + + if (inptr > start && *(inptr - 1) == (byte) '\r') + length--; + + // consume the '\n' + inptr++; + + if (inptr >= inend) { + // we don't have enough input data + inputIndex = (int) (start - inbuf); + left = (int) (inptr - start); + needInput = true; + break; + } + + lineNumber++; + + if (length >= 5 && IsMboxMarker (start)) { + int startIndex = (int) (start - inbuf); + + mboxMarkerOffset = GetOffset (startIndex); + mboxMarkerLength = (int) length; + + OnMboxMarkerBegin (mboxMarkerOffset); + OnMboxMarkerEnd (mboxMarkerOffset + length); + + if (mboxMarkerBuffer.Length < mboxMarkerLength) + Array.Resize (ref mboxMarkerBuffer, mboxMarkerLength); + + Buffer.BlockCopy (input, startIndex, mboxMarkerBuffer, 0, (int) length); + complete = true; + break; + } + } + + if (!needInput) { + inputIndex = (int) (inptr - inbuf); + left = 0; + } + } + + unsafe void StepMboxMarker (byte* inbuf, CancellationToken cancellationToken) + { + bool complete = false; + bool needInput; + int left = 0; + + mboxMarkerLength = 0; + + do { + var available = ReadAhead (Math.Max (ReadAheadSize, left), 0, cancellationToken); + + if (available <= left) { + // failed to find a From line; EOF reached + state = MimeParserState.Error; + inputIndex = inputEnd; + return; + } + + needInput = false; + + StepMboxMarker (inbuf, ref needInput, ref complete, ref left); + } while (!complete); + + state = MimeParserState.MessageHeaders; + } + + void AppendRawHeaderData (int startIndex, int length) + { + int left = headerBuffer.Length - headerIndex; + + if (left < length) + Array.Resize (ref headerBuffer, NextAllocSize (headerIndex + length)); + + Buffer.BlockCopy (input, startIndex, headerBuffer, headerIndex, length); + headerIndex += length; + } + + void ResetRawHeaderData () + { + preHeaderLength = 0; + headerIndex = 0; + } + + unsafe void ParseAndAppendHeader () + { + if (headerIndex == 0) + return; + + fixed (byte* buf = headerBuffer) { + if (Header.TryParse (options, buf, headerIndex, false, out var header)) { + header.Offset = headerOffset; + headers.Add (header); + headerIndex = 0; + } + } + } + + static bool IsControl (byte c) + { + return c.IsCtrl (); + } + + static bool IsBlank (byte c) + { + return c.IsBlank (); + } + + static unsafe bool IsEoln (byte *text) + { + if (*text == (byte) '\r') + text++; + + return *text == (byte) '\n'; + } + + unsafe bool StepHeaders (byte* inbuf, ref bool scanningFieldName, ref bool checkFolded, ref bool midline, + ref bool blank, ref bool valid, ref int left) + { + byte* inptr = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + bool needInput = false; + long length; + bool eoln; + + *inend = (byte) '\n'; + + while (inptr < inend) { + byte* start = inptr; + + // if we are scanning a new line, check for a folded header + if (!midline && checkFolded && !IsBlank (*inptr)) { + ParseAndAppendHeader (); + + headerOffset = GetOffset ((int) (inptr - inbuf)); + scanningFieldName = true; + checkFolded = false; + blank = false; + valid = true; + } + + eoln = IsEoln (inptr); + if (scanningFieldName && !eoln) { + // scan and validate the field name + if (*inptr != (byte) ':') { + *inend = (byte) ':'; + + while (*inptr != (byte) ':') { + // Blank spaces are allowed between the field name and + // the ':', but field names themselves are not allowed + // to contain spaces. + if (IsBlank (*inptr)) { + blank = true; + } else if (blank || IsControl (*inptr)) { + valid = false; + break; + } + + inptr++; + } + + if (inptr == inend) { + // we don't have enough input data; restore state back to the beginning of the line + left = (int) (inend - start); + inputIndex = (int) (start - inbuf); + needInput = true; + break; + } + + *inend = (byte) '\n'; + } else { + valid = false; + } + + if (!valid) { + length = inptr - start; + + if (format == MimeFormat.Mbox && inputIndex >= contentEnd && length >= 5 && IsMboxMarker (start)) { + // we've found the start of the next message... + inputIndex = (int) (start - inbuf); + state = MimeParserState.Complete; + headerIndex = 0; + return false; + } + + if (headers.Count == 0) { + if (state == MimeParserState.MessageHeaders) { + // ignore From-lines that might appear at the start of a message + if (toplevel && (length < 5 || !IsMboxMarker (start, true))) { + // not a From-line... + inputIndex = (int) (start - inbuf); + state = MimeParserState.Error; + headerIndex = 0; + return false; + } + } else if (toplevel && state == MimeParserState.Headers) { + inputIndex = (int) (start - inbuf); + state = MimeParserState.Error; + headerIndex = 0; + return false; + } + } + } + } + + scanningFieldName = false; + + while (*inptr != (byte) '\n') + inptr++; + + if (inptr == inend) { + // we didn't manage to slurp up a full line, save what we have and refill our input buffer + length = inptr - start; + + if (inptr > start) { + // Note: if the last byte we got was a '\r', rewind a byte + inptr--; + if (*inptr == (byte) '\r') + length--; + else + inptr++; + } + + if (length > 0) { + AppendRawHeaderData ((int) (start - inbuf), (int) length); + midline = true; + } + + inputIndex = (int) (inptr - inbuf); + left = (int) (inend - inptr); + needInput = true; + break; + } + + lineNumber++; + + // check to see if we've reached the end of the headers + if (!midline && IsEoln (start)) { + inputIndex = (int) (inptr - inbuf) + 1; + state = MimeParserState.Content; + ParseAndAppendHeader (); + headerIndex = 0; + return false; + } + + length = (inptr + 1) - start; + + if ((boundary = CheckBoundary ((int) (start - inbuf), start, (int) length)) != BoundaryType.None) { + inputIndex = (int) (start - inbuf); + state = MimeParserState.Boundary; + headerIndex = 0; + return false; + } + + if (!valid && headers.Count == 0) { + if (length > 0 && preHeaderLength == 0) { + if (inptr[-1] == (byte) '\r') + length--; + length--; + + preHeaderLength = (int) length; + + if (preHeaderLength > preHeaderBuffer.Length) + Array.Resize (ref preHeaderBuffer, NextAllocSize (preHeaderLength)); + + Buffer.BlockCopy (input, (int) (start - inbuf), preHeaderBuffer, 0, preHeaderLength); + } + scanningFieldName = true; + checkFolded = false; + blank = false; + valid = true; + } else { + AppendRawHeaderData ((int) (start - inbuf), (int) length); + checkFolded = true; + } + + midline = false; + inptr++; + } + + if (!needInput) { + inputIndex = (int) (inptr - inbuf); + left = (int) (inend - inptr); + } + + return true; + } + + unsafe void StepHeaders (byte* inbuf, CancellationToken cancellationToken) + { + bool scanningFieldName = true; + bool checkFolded = false; + bool midline = false; + bool blank = false; + bool valid = true; + int left = 0; + + headerBlockBegin = GetOffset (inputIndex); + boundary = BoundaryType.None; + ResetRawHeaderData (); + headers.Clear (); + + ReadAhead (ReadAheadSize, 0, cancellationToken); + + do { + if (!StepHeaders (inbuf, ref scanningFieldName, ref checkFolded, ref midline, ref blank, ref valid, ref left)) + break; + + var available = ReadAhead (left + 1, 0, cancellationToken); + + if (available == left) { + // EOF reached before we reached the end of the headers... + if (scanningFieldName && left > 0) { + // EOF reached right in the middle of a header field name. Throw an error. + // + // See private email from Feb 8, 2018 which contained a sample message w/o + // any breaks between the header and message body. The file also did not + // end with a newline sequence. + state = MimeParserState.Error; + } else { + // EOF reached somewhere in the middle of the value. + // + // Append whatever data we've got left and pretend we found the end + // of the header value (and the header block). + // + // For more details, see https://github.com/jstedfast/MimeKit/pull/51 + // and https://github.com/jstedfast/MimeKit/issues/348 + if (left > 0) { + AppendRawHeaderData (inputIndex, left); + inputIndex = inputEnd; + } + + ParseAndAppendHeader (); + + headerBlockEnd = GetOffset (inputIndex); + state = MimeParserState.Content; + } + break; + } + } while (true); + + headerBlockEnd = GetOffset (inputIndex); + } + + unsafe bool SkipLine (byte* inbuf, bool consumeNewLine) + { + byte* inptr = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + + *inend = (byte) '\n'; + + while (*inptr != (byte) '\n') + inptr++; + + if (inptr < inend) { + inputIndex = (int) (inptr - inbuf); + + if (consumeNewLine) { + inputIndex++; + lineNumber++; + } else if (*(inptr - 1) == (byte) '\r') { + inputIndex--; + } + + return true; + } + + inputIndex = inputEnd; + + return false; + } + + unsafe bool SkipLine (byte* inbuf, bool consumeNewLine, CancellationToken cancellationToken) + { + do { + if (SkipLine (inbuf, consumeNewLine)) + return true; + + if (ReadAhead (ReadAheadSize, 1, cancellationToken) <= 0) + return false; + } while (true); + } + + unsafe MimeParserState Step (byte* inbuf, CancellationToken cancellationToken) + { + switch (state) { + case MimeParserState.Initialized: + if (!StepByteOrderMark (inbuf, cancellationToken)) { + state = MimeParserState.Eos; + break; + } + + state = format == MimeFormat.Mbox ? MimeParserState.MboxMarker : MimeParserState.MessageHeaders; + break; + case MimeParserState.MboxMarker: + StepMboxMarker (inbuf, cancellationToken); + break; + case MimeParserState.MessageHeaders: + case MimeParserState.Headers: + StepHeaders (inbuf, cancellationToken); + toplevel = false; + break; + } + + return state; + } + + ContentType GetContentType (ContentType parent) + { + for (int i = 0; i < headers.Count; i++) { + if (!headers[i].Field.Equals ("Content-Type", StringComparison.OrdinalIgnoreCase)) + continue; + + var rawValue = headers[i].RawValue; + int index = 0; + + if (!ContentType.TryParse (options, rawValue, ref index, rawValue.Length, false, out var type) && type == null) { + // if 'type' is null, then it means that even the mime-type was unintelligible + type = new ContentType ("application", "octet-stream"); + + // attempt to recover any parameters... + while (index < rawValue.Length && rawValue[index] != ';') + index++; + + if (++index < rawValue.Length) { + if (ParameterList.TryParse (options, rawValue, ref index, rawValue.Length, false, out var parameters)) + type.Parameters = parameters; + } + } + + return type; + } + + if (parent == null || !parent.IsMimeType ("multipart", "digest")) + return new ContentType ("text", "plain"); + + return new ContentType ("message", "rfc822"); + } + + unsafe bool IsPossibleBoundary (byte* text, int length) + { + if (length < 2) + return false; + + if (*text == (byte) '-' && *(text + 1) == (byte) '-') + return true; + + if (format == MimeFormat.Mbox && length >= 5 && IsMboxMarker (text)) + return true; + + return false; + } + + static unsafe bool IsBoundary (byte* text, int length, byte[] boundary, int boundaryLength) + { + if (boundaryLength > length) + return false; + + fixed (byte* boundaryptr = boundary) { + // make sure that the text matches the boundary + if (!CStringsEqual (text, boundaryptr, boundaryLength)) + return false; + + // if this is an mbox marker, we're done + if (IsMboxMarker (text)) + return true; + + // the boundary may optionally be followed by lwsp + byte* inptr = text + boundaryLength; + byte* inend = text + length; + + while (inptr < inend) { + if (!(*inptr).IsWhitespace ()) + return false; + + inptr++; + } + } + + return true; + } + + unsafe BoundaryType CheckBoundary (int startIndex, byte* start, int length) + { + int count = bounds.Count; + + if (!IsPossibleBoundary (start, length)) + return BoundaryType.None; + + if (contentEnd > 0) { + // We'll need to special-case checking for the mbox From-marker when respecting Content-Length + count--; + } + + for (int i = 0; i < count; i++) { + var boundary = bounds[i]; + + if (IsBoundary (start, length, boundary.Marker, boundary.FinalLength)) + return i == 0 ? BoundaryType.ImmediateEndBoundary : BoundaryType.ParentEndBoundary; + + if (IsBoundary (start, length, boundary.Marker, boundary.Length)) + return i == 0 ? BoundaryType.ImmediateBoundary : BoundaryType.ParentBoundary; + } + + if (contentEnd > 0) { + // now it is time to check the mbox From-marker for the Content-Length case + long curOffset = GetOffset (startIndex); + var boundary = bounds[count]; + + if (curOffset >= contentEnd && IsBoundary (start, length, boundary.Marker, boundary.Length)) + return BoundaryType.ImmediateEndBoundary; + } + + return BoundaryType.None; + } + + unsafe bool FoundImmediateBoundary (byte* inbuf, bool final) + { + int boundaryLength = final ? bounds[0].FinalLength : bounds[0].Length; + byte* start = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + byte *inptr = start; + + *inend = (byte) '\n'; + + while (*inptr != (byte) '\n') + inptr++; + + return IsBoundary (start, (int) (inptr - start), bounds[0].Marker, boundaryLength); + } + + int GetMaxBoundaryLength () + { + return bounds.Count > 0 ? bounds[0].MaxLength + 2 : 0; + } + + unsafe void ScanContent (byte* inbuf, ref int contentIndex, ref int nleft, ref bool midline, ref bool[] formats) + { + int length = inputEnd - inputIndex; + byte* inptr = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + int startIndex = inputIndex; + + contentIndex = inputIndex; + + if (midline && length == nleft) + boundary = BoundaryType.Eos; + + *inend = (byte) '\n'; + + while (inptr < inend) { + // Note: we can always depend on byte[] arrays being 4-byte aligned on 32bit and 64bit architectures + int alignment = (startIndex + 3) & ~3; + byte* aligned = inbuf + alignment; + byte* start = inptr; + byte c = *aligned; + uint mask; + + *aligned = (byte) '\n'; + while (*inptr != (byte) '\n') + inptr++; + *aligned = c; + + if (inptr == aligned && c != (byte) '\n') { + // -funroll-loops, bitches. + uint* dword = (uint*) inptr; + + do { + mask = *dword++ ^ 0x0A0A0A0A; + mask = ((mask - 0x01010101) & (~mask & 0x80808080)); + } while (mask == 0); + + inptr = (byte*) (dword - 1); + while (*inptr != (byte) '\n') + inptr++; + } + + length = (int) (inptr - start); + + if (inptr < inend) { + if ((boundary = CheckBoundary (startIndex, start, length)) != BoundaryType.None) + break; + + if (length > 0 && *(inptr - 1) == (byte) '\r') + formats[(int) NewLineFormat.Dos] = true; + else + formats[(int) NewLineFormat.Unix] = true; + + lineNumber++; + length++; + inptr++; + } else { + // didn't find the end of the line... + midline = true; + + if (boundary == BoundaryType.None) { + // not enough to tell if we found a boundary + break; + } + + if ((boundary = CheckBoundary (startIndex, start, length)) != BoundaryType.None) + break; + } + + startIndex += length; + } + + inputIndex = startIndex; + } + + class ScanContentResult + { + public readonly NewLineFormat? Format; + public readonly bool IsEmpty; + + public ScanContentResult (bool[] formats, bool isEmpty) + { + if (formats[(int) NewLineFormat.Unix] && formats[(int) NewLineFormat.Dos]) + Format = NewLineFormat.Mixed; + else if (formats[(int) NewLineFormat.Unix]) + Format = NewLineFormat.Unix; + else if (formats[(int) NewLineFormat.Dos]) + Format = NewLineFormat.Dos; + else + Format = null; + IsEmpty = isEmpty; + } + } + + unsafe ScanContentResult ScanContent (byte* inbuf, Stream content, bool trimNewLine, CancellationToken cancellationToken) + { + int atleast = Math.Max (ReadAheadSize, GetMaxBoundaryLength ()); + int contentIndex = inputIndex; + var formats = new bool[2]; + bool midline = false; + int nleft; + + do { + if (contentIndex < inputIndex) + content.Write (input, contentIndex, inputIndex - contentIndex); + + nleft = inputEnd - inputIndex; + if (ReadAhead (atleast, 2, cancellationToken) <= 0) { + boundary = BoundaryType.Eos; + contentIndex = inputIndex; + break; + } + + ScanContent (inbuf, ref contentIndex, ref nleft, ref midline, ref formats); + } while (boundary == BoundaryType.None); + + if (contentIndex < inputIndex) + content.Write (input, contentIndex, inputIndex - contentIndex); + + var isEmpty = content.Length == 0; + + if (boundary != BoundaryType.Eos && trimNewLine) { + // the last \r\n belongs to the boundary + if (content.Length > 0) { + if (input[inputIndex - 2] == (byte) '\r') + content.SetLength (content.Length - 2); + else + content.SetLength (content.Length - 1); + } + } + + return new ScanContentResult (formats, isEmpty); + } + + unsafe void ConstructMimePart (MimePart part, byte* inbuf, CancellationToken cancellationToken) + { + long endOffset, beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + ScanContentResult result; + Stream content; + + OnMimeContentBegin (part, beginOffset); + + if (persistent) { + using (var measured = new MeasuringStream ()) { + result = ScanContent (inbuf, measured, true, cancellationToken); + endOffset = beginOffset + measured.Length; + } + + content = new BoundStream (stream, beginOffset, endOffset, true); + } else { + content = new MemoryBlockStream (); + result = ScanContent (inbuf, content, true, cancellationToken); + content.Seek (0, SeekOrigin.Begin); + endOffset = beginOffset + content.Length; + } + + OnMimeContentEnd (part, endOffset); + OnMimeContentOctets (part, endOffset - beginOffset); + OnMimeContentLines (part, lineNumber - beginLineNumber); + + if (!result.IsEmpty) + part.Content = new MimeContent (content, part.ContentTransferEncoding) { NewLineFormat = result.Format }; + else + content.Dispose (); + } + + unsafe void ConstructMessagePart (MessagePart rfc822, byte* inbuf, int depth, CancellationToken cancellationToken) + { + var beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + + OnMimeContentBegin (rfc822, beginOffset); + + if (bounds.Count > 0) { + int atleast = Math.Max (ReadAheadSize, GetMaxBoundaryLength ()); + + if (ReadAhead (atleast, 0, cancellationToken) <= 0) { + boundary = BoundaryType.Eos; + return; + } + + byte* start = inbuf + inputIndex; + byte* inend = inbuf + inputEnd; + byte* inptr = start; + + *inend = (byte) '\n'; + + while (*inptr != (byte) '\n') + inptr++; + + boundary = CheckBoundary (inputIndex, start, (int) (inptr - start)); + + switch (boundary) { + case BoundaryType.ImmediateEndBoundary: + case BoundaryType.ImmediateBoundary: + case BoundaryType.ParentBoundary: + return; + case BoundaryType.ParentEndBoundary: + // ignore "From " boundaries, broken mailers tend to include these... + if (!IsMboxMarker (start)) + return; + break; + } + } + + // parse the headers... + state = MimeParserState.MessageHeaders; + if (Step (inbuf, cancellationToken) == MimeParserState.Error) { + // Note: this either means that StepHeaders() found the end of the stream + // or an invalid header field name at the start of the message headers, + // which likely means that this is not a valid MIME stream? + boundary = BoundaryType.Eos; + return; + } + + var message = new MimeMessage (options, headers, RfcComplianceMode.Loose); + var type = GetContentType (null); + + if (preHeaderBuffer.Length > 0) { + message.MboxMarker = new byte[preHeaderLength]; + Buffer.BlockCopy (preHeaderBuffer, 0, message.MboxMarker, 0, preHeaderLength); + } + + var entity = options.CreateEntity (type, headers, true, depth); + message.Body = entity; + + OnMimeMessageBegin (message, headerBlockBegin); + OnMimeMessageHeadersEnd (message, headerBlockEnd); + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + ConstructMultipart ((Multipart) entity, inbuf, depth + 1, cancellationToken); + else if (entity is MessagePart) + ConstructMessagePart ((MessagePart) entity, inbuf, depth + 1, cancellationToken); + else + ConstructMimePart ((MimePart) entity, inbuf, cancellationToken); + + rfc822.Message = message; + + var endOffset = GetOffset (inputIndex); + OnMimeEntityEnd (entity, endOffset); + OnMimeMessageEnd (message, endOffset); + OnMimeContentEnd (rfc822, endOffset); + OnMimeContentOctets (rfc822, endOffset - beginOffset); + OnMimeContentLines (rfc822, lineNumber - beginLineNumber); + } + + unsafe void MultipartScanPreamble (Multipart multipart, byte* inbuf, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + long offset = GetOffset (inputIndex); + + OnMultipartPreambleBegin (multipart, offset); + ScanContent (inbuf, memory, false, cancellationToken); + multipart.RawPreamble = memory.ToArray (); + OnMultipartPreambleEnd (multipart, offset + memory.Length); + } + } + + unsafe void MultipartScanEpilogue (Multipart multipart, byte* inbuf, CancellationToken cancellationToken) + { + using (var memory = new MemoryStream ()) { + long offset = GetOffset (inputIndex); + + OnMultipartEpilogueBegin (multipart, offset); + var result = ScanContent (inbuf, memory, true, cancellationToken); + multipart.RawEpilogue = result.IsEmpty ? null : memory.ToArray (); + OnMultipartEpilogueEnd (multipart, offset + memory.Length); + } + } + + unsafe void MultipartScanSubparts (Multipart multipart, byte* inbuf, int depth, CancellationToken cancellationToken) + { + do { + OnMultipartBoundaryBegin (multipart, GetOffset (inputIndex)); + + // skip over the boundary marker + if (!SkipLine (inbuf, true, cancellationToken)) { + OnMultipartBoundaryEnd (multipart, GetOffset (inputIndex)); + boundary = BoundaryType.Eos; + break; + } + + OnMultipartBoundaryEnd (multipart, GetOffset (inputIndex)); + + // parse the headers + state = MimeParserState.Headers; + if (Step (inbuf, cancellationToken) == MimeParserState.Error) { + boundary = BoundaryType.Eos; + break; + } + + if (state == MimeParserState.Boundary) { + if (headers.Count == 0) { + if (boundary == BoundaryType.ImmediateBoundary) + continue; + break; + } + + // This part has no content, but that will be handled in ConstructMultipart() + // or ConstructMimePart(). + } + + //if (state == ParserState.Complete && headers.Count == 0) + // return BoundaryType.EndBoundary; + + var type = GetContentType (multipart.ContentType); + var entity = options.CreateEntity (type, headers, false, depth); + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + ConstructMultipart ((Multipart) entity, inbuf, depth + 1, cancellationToken); + else if (entity is MessagePart) + ConstructMessagePart ((MessagePart) entity, inbuf, depth + 1, cancellationToken); + else + ConstructMimePart ((MimePart) entity, inbuf, cancellationToken); + + OnMimeEntityEnd (entity, GetOffset (inputIndex)); + + multipart.Add (entity); + } while (boundary == BoundaryType.ImmediateBoundary); + } + + void PushBoundary (string boundary) + { + if (bounds.Count > 0) + bounds.Insert (0, new Boundary (boundary, bounds[0].MaxLength)); + else + bounds.Add (new Boundary (boundary, 0)); + } + + void PopBoundary () + { + bounds.RemoveAt (0); + } + + unsafe void ConstructMultipart (Multipart multipart, byte* inbuf, int depth, CancellationToken cancellationToken) + { + var beginOffset = GetOffset (inputIndex); + var beginLineNumber = lineNumber; + var marker = multipart.Boundary; + long endOffset; + + OnMimeContentBegin (multipart, beginOffset); + + if (marker == null) { +#if DEBUG + Debug.WriteLine ("Multipart without a boundary encountered!"); +#endif + + // Note: this will scan all content into the preamble... + MultipartScanPreamble (multipart, inbuf, cancellationToken); + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + return; + } + + PushBoundary (marker); + + MultipartScanPreamble (multipart, inbuf, cancellationToken); + if (boundary == BoundaryType.ImmediateBoundary) + MultipartScanSubparts (multipart, inbuf, depth, cancellationToken); + + if (boundary == BoundaryType.ImmediateEndBoundary) { + OnMultipartEndBoundaryBegin (multipart, GetOffset (inputIndex)); + + // consume the end boundary and read the epilogue (if there is one) + multipart.WriteEndBoundary = true; + SkipLine (inbuf, false, cancellationToken); + PopBoundary (); + + OnMultipartEndBoundaryEnd (multipart, GetOffset (inputIndex)); + + MultipartScanEpilogue (multipart, inbuf, cancellationToken); + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + return; + } + + endOffset = GetOffset (inputIndex); + + OnMimeContentEnd (multipart, endOffset); + OnMimeContentOctets (multipart, endOffset - beginOffset); + OnMimeContentLines (multipart, lineNumber - beginLineNumber); + + multipart.WriteEndBoundary = false; + + // We either found the end of the stream or we found a parent's boundary + PopBoundary (); + + if (boundary == BoundaryType.ParentEndBoundary && FoundImmediateBoundary (inbuf, true)) + boundary = BoundaryType.ImmediateEndBoundary; + else if (boundary == BoundaryType.ParentBoundary && FoundImmediateBoundary (inbuf, false)) + boundary = BoundaryType.ImmediateBoundary; + } + + unsafe HeaderList ParseHeaders (byte* inbuf, CancellationToken cancellationToken) + { + state = MimeParserState.Headers; + if (Step (inbuf, cancellationToken) == MimeParserState.Error) + throw new FormatException ("Failed to parse headers."); + + state = eos ? MimeParserState.Eos : MimeParserState.Complete; + + var parsed = new HeaderList (options); + foreach (var header in headers) + parsed.Add (header); + + return parsed; + } + + /// + /// Parses a list of headers from the stream. + /// + /// + /// Parses a list of headers from the stream. + /// + /// The parsed list of headers. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the headers. + /// + /// + /// An I/O error occurred. + /// + public HeaderList ParseHeaders (CancellationToken cancellationToken = default (CancellationToken)) + { + unsafe { + fixed (byte* inbuf = input) { + return ParseHeaders (inbuf, cancellationToken); + } + } + } + + unsafe MimeEntity ParseEntity (byte* inbuf, CancellationToken cancellationToken) + { + // Note: if a previously parsed MimePart's content has been read, + // then the stream position will have moved and will need to be + // reset. + if (persistent && stream.Position != position) + stream.Seek (position, SeekOrigin.Begin); + + state = MimeParserState.Headers; + toplevel = true; + + if (Step (inbuf, cancellationToken) == MimeParserState.Error) + throw new FormatException ("Failed to parse entity headers."); + + var type = GetContentType (null); + + // Note: we pass 'false' as the 'toplevel' argument here because + // we want the entity to consume all of the headers. + var entity = options.CreateEntity (type, headers, false, 0); + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + ConstructMultipart ((Multipart) entity, inbuf, 0, cancellationToken); + else if (entity is MessagePart) + ConstructMessagePart ((MessagePart) entity, inbuf, 0, cancellationToken); + else + ConstructMimePart ((MimePart) entity, inbuf, cancellationToken); + + OnMimeEntityEnd (entity, GetOffset (inputIndex)); + + if (boundary != BoundaryType.Eos) + state = MimeParserState.Complete; + else + state = MimeParserState.Eos; + + return entity; + } + + /// + /// Parses an entity from the stream. + /// + /// + /// Parses an entity from the stream. + /// + /// The parsed entity. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the entity. + /// + /// + /// An I/O error occurred. + /// + public MimeEntity ParseEntity (CancellationToken cancellationToken = default (CancellationToken)) + { + unsafe { + fixed (byte* inbuf = input) { + return ParseEntity (inbuf, cancellationToken); + } + } + } + + unsafe MimeMessage ParseMessage (byte* inbuf, CancellationToken cancellationToken) + { + // Note: if a previously parsed MimePart's content has been read, + // then the stream position will have moved and will need to be + // reset. + if (persistent && stream.Position != position) + stream.Seek (position, SeekOrigin.Begin); + + // scan the from-line if we are parsing an mbox + while (state != MimeParserState.MessageHeaders) { + switch (Step (inbuf, cancellationToken)) { + case MimeParserState.Error: + throw new FormatException ("Failed to find mbox From marker."); + case MimeParserState.Eos: + throw new FormatException ("End of stream."); + } + } + + toplevel = true; + + // parse the headers + if (state < MimeParserState.Content && Step (inbuf, cancellationToken) == MimeParserState.Error) + throw new FormatException ("Failed to parse message headers."); + + var message = new MimeMessage (options, headers, RfcComplianceMode.Loose); + + OnMimeMessageBegin (message, headerBlockBegin); + OnMimeMessageHeadersEnd (message, headerBlockEnd); + + contentEnd = 0; + if (format == MimeFormat.Mbox && options.RespectContentLength) { + for (int i = 0; i < headers.Count; i++) { + if (headers[i].Id != HeaderId.ContentLength) + continue; + + var value = headers[i].RawValue; + int length, index = 0; + + if (!ParseUtils.SkipWhiteSpace (value, ref index, value.Length)) + continue; + + if (!ParseUtils.TryParseInt32 (value, ref index, value.Length, out length)) + continue; + + contentEnd = GetOffset (inputIndex) + length; + break; + } + } + + var type = GetContentType (null); + var entity = options.CreateEntity (type, headers, true, 0); + message.Body = entity; + + OnMimeEntityBegin (entity, headerBlockBegin); + OnMimeEntityHeadersEnd (entity, headerBlockEnd); + + if (entity is Multipart) + ConstructMultipart ((Multipart) entity, inbuf, 0, cancellationToken); + else if (entity is MessagePart) + ConstructMessagePart ((MessagePart) entity, inbuf, 0, cancellationToken); + else + ConstructMimePart ((MimePart) entity, inbuf, cancellationToken); + + var endOffset = GetOffset (inputIndex); + OnMimeEntityEnd (entity, endOffset); + OnMimeMessageEnd (message, endOffset); + + if (boundary != BoundaryType.Eos) { + if (format == MimeFormat.Mbox) + state = MimeParserState.MboxMarker; + else + state = MimeParserState.Complete; + } else { + state = MimeParserState.Eos; + } + + return message; + } + + /// + /// Parses a message from the stream. + /// + /// + /// Parses a message from the stream. + /// + /// The parsed message. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// There was an error parsing the message. + /// + /// + /// An I/O error occurred. + /// + public MimeMessage ParseMessage (CancellationToken cancellationToken = default (CancellationToken)) + { + unsafe { + fixed (byte* inbuf = input) { + return ParseMessage (inbuf, cancellationToken); + } + } + } + + #region IEnumerable implementation + + /// + /// Enumerates the messages in the stream. + /// + /// + /// This is mostly useful when parsing mbox-formatted streams. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + while (!IsEndOfStream) + yield return ParseMessage (); + + yield break; + } + + #endregion + + #region IEnumerable implementation + + /// + /// Enumerates the messages in the stream. + /// + /// + /// This is mostly useful when parsing mbox-formatted streams. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/src/MimeKit/MimePart.cs b/src/MimeKit/MimePart.cs new file mode 100644 index 0000000..39838b2 --- /dev/null +++ b/src/MimeKit/MimePart.cs @@ -0,0 +1,761 @@ +// +// MimePart.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using MD5 = System.Security.Cryptography.MD5; + +using MimeKit.IO.Filters; +using MimeKit.Encodings; +using MimeKit.Utils; +using MimeKit.IO; + +namespace MimeKit { + /// + /// A leaf-node MIME part that contains content such as the message body text or an attachment. + /// + /// + /// A leaf-node MIME part that contains content such as the message body text or an attachment. + /// + /// + /// + /// + public class MimePart : MimeEntity + { + static readonly string[] ContentTransferEncodings = { + null, "7bit", "8bit", "binary", "base64", "quoted-printable", "x-uuencode" + }; + ContentEncoding encoding; + string md5sum; + int? duration; + + /// + /// Initialize a new instance of the class + /// based on the . + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MimePart (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class + /// with the specified media type and subtype. + /// + /// + /// Creates a new with the specified media type and subtype. + /// + /// The media type. + /// The media subtype. + /// An array of initialization parameters: headers and part content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// contains more than one or + /// . + /// -or- + /// contains one or more arguments of an unknown type. + /// + public MimePart (string mediaType, string mediaSubtype, params object[] args) : this (mediaType, mediaSubtype) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + IMimeContent content = null; + + foreach (object obj in args) { + if (obj == null || TryInit (obj)) + continue; + + if (obj is IMimeContent co) { + if (content != null) + throw new ArgumentException ("IMimeContent should not be specified more than once."); + + content = co; + continue; + } + + if (obj is Stream stream) { + if (content != null) + throw new ArgumentException ("Stream (used as content) should not be specified more than once."); + + // Use default as specified by ContentObject ctor when building a new MimePart. + content = new MimeContent (stream); + continue; + } + + throw new ArgumentException ("Unknown initialization parameter: " + obj.GetType ()); + } + + if (content != null) + Content = content; + } + + /// + /// Initialize a new instance of the class + /// with the specified media type and subtype. + /// + /// + /// Creates a new with the specified media type and subtype. + /// + /// The media type. + /// The media subtype. + /// + /// is null. + /// -or- + /// is null. + /// + public MimePart (string mediaType, string mediaSubtype) : base (mediaType, mediaSubtype) + { + } + + /// + /// Initialize a new instance of the class + /// with the specified content type. + /// + /// + /// Creates a new with the specified Content-Type value. + /// + /// The content type. + /// + /// is null. + /// + public MimePart (ContentType contentType) : base (contentType) + { + } + + /// + /// Initialize a new instance of the class + /// with the specified content type. + /// + /// + /// Creates a new with the specified Content-Type value. + /// + /// The content type. + /// + /// is null. + /// + /// + /// could not be parsed. + /// + public MimePart (string contentType) : base (ContentType.Parse (contentType)) + { + } + + /// + /// Initialize a new instance of the class + /// with the default Content-Type of application/octet-stream. + /// + /// + /// Creates a new with a Content-Type of application/octet-stream. + /// + public MimePart () : this ("application", "octet-stream") + { + } + + /// + /// Gets or sets the duration of the content if available. + /// + /// + /// The Content-Duration header specifies duration of timed media, + /// such as audio or video, in seconds. + /// + /// The duration of the content. + /// + /// is negative. + /// + public int? ContentDuration { + get { return duration; } + set { + if (duration == value) + return; + + if (value.HasValue && value.Value < 0) + throw new ArgumentOutOfRangeException (nameof (value)); + + duration = value; + + if (value.HasValue) + SetHeader ("Content-Duration", value.Value.ToString ()); + else + RemoveHeader ("Content-Duration"); + } + } + + /// + /// Gets or sets the md5sum of the content. + /// + /// + /// The Content-MD5 header specifies the base64-encoded MD5 checksum of the content + /// in its canonical format. + /// For more information, see rfc1864. + /// + /// The md5sum of the content. + public string ContentMd5 { + get { return md5sum; } + set { + if (md5sum == value) + return; + + md5sum = value != null ? value.Trim () : null; + + if (value != null) + SetHeader ("Content-Md5", md5sum); + else + RemoveHeader ("Content-Md5"); + } + } + + /// + /// Gets or sets the content transfer encoding. + /// + /// + /// The Content-Transfer-Encoding header specifies an auxiliary encoding + /// that was applied to the content in order to allow it to pass through + /// mail transport mechanisms (such as SMTP) which may have limitations + /// in the byte ranges that it accepts. For example, many SMTP servers + /// do not accept data outside of the 7-bit ASCII range and so sending + /// binary attachments or even non-English text is not possible without + /// applying an encoding such as base64 or quoted-printable. + /// + /// The content transfer encoding. + /// + /// is not a valid content encoding. + /// + public ContentEncoding ContentTransferEncoding { + get { return encoding; } + set { + if (encoding == value) + return; + + int index = (int) value; + + if (index < 0 || index >= ContentTransferEncodings.Length) + throw new ArgumentOutOfRangeException (nameof (value)); + + var text = ContentTransferEncodings[index]; + + encoding = value; + + if (text != null) + SetHeader ("Content-Transfer-Encoding", text); + else + RemoveHeader ("Content-Transfer-Encoding"); + } + } + + /// + /// Gets or sets the name of the file. + /// + /// + /// First checks for the "filename" parameter on the Content-Disposition header. If + /// that does not exist, then the "name" parameter on the Content-Type header is used. + /// When setting the filename, both the "filename" parameter on the Content-Disposition + /// header and the "name" parameter on the Content-Type header are set. + /// + /// + /// + /// + /// The name of the file. + public string FileName { + get { + string filename = null; + + if (ContentDisposition != null) + filename = ContentDisposition.FileName; + + if (filename == null) + filename = ContentType.Name; + + return filename != null ? filename.Trim () : null; + } + set { + if (value != null) { + if (ContentDisposition == null) + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment); + ContentDisposition.FileName = value; + } else if (ContentDisposition != null) { + ContentDisposition.FileName = value; + } + + ContentType.Name = value; + } + } + + /// + /// Gets or sets the MIME content. + /// + /// + /// Gets or sets the MIME content. + /// + /// + /// + /// + /// The MIME content. + public IMimeContent Content { + get; set; + } + + /// + /// Gets or sets the MIME content. + /// + /// + /// Gets or sets the MIME content. + /// + /// The MIME content. + [Obsolete ("Use the Content property instead.")] + public IMimeContent ContentObject { + get { return Content; } + set { Content = value; } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMimePart (this); + } + + /// + /// Calculates the most efficient content encoding given the specified constraint. + /// + /// + /// If no is set, will be returned. + /// + /// The most efficient content encoding. + /// The encoding constraint. + /// The cancellation token. + /// + /// is not a valid value. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public ContentEncoding GetBestEncoding (EncodingConstraint constraint, CancellationToken cancellationToken = default (CancellationToken)) + { + return GetBestEncoding (constraint, 78, cancellationToken); + } + + /// + /// Calculates the most efficient content encoding given the specified constraint. + /// + /// + /// If no is set, will be returned. + /// + /// The most efficient content encoding. + /// The encoding constraint. + /// The maximum allowable length for a line (not counting the CRLF). Must be between 72 and 998 (inclusive). + /// The cancellation token. + /// + /// is not between 72 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public ContentEncoding GetBestEncoding (EncodingConstraint constraint, int maxLineLength, CancellationToken cancellationToken = default (CancellationToken)) + { + if (Content == null) + return ContentEncoding.SevenBit; + + using (var measure = new MeasuringStream ()) { + using (var filtered = new FilteredStream (measure)) { + var filter = new BestEncodingFilter (); + + filtered.Add (filter); + Content.DecodeTo (filtered, cancellationToken); + filtered.Flush (); + + return filter.GetBestEncoding (constraint, maxLineLength); + } + } + } + + /// + /// Computes the MD5 checksum of the content. + /// + /// + /// Computes the MD5 checksum of the MIME content in its canonical + /// format and then base64-encodes the result. + /// + /// The md5sum of the content. + /// + /// The is null. + /// + public string ComputeContentMd5 () + { + if (Content == null) + throw new InvalidOperationException ("Cannot compute Md5 checksum without a ContentObject."); + + using (var stream = Content.Open ()) { + byte[] checksum; + + using (var filtered = new FilteredStream (stream)) { + if (ContentType.IsMimeType ("text", "*")) + filtered.Add (new Unix2DosFilter ()); + + using (var md5 = MD5.Create ()) + checksum = md5.ComputeHash (filtered); + } + + var base64 = new Base64Encoder (true); + var digest = new byte[base64.EstimateOutputLength (checksum.Length)]; + int n = base64.Flush (checksum, 0, checksum.Length, digest); + + return Encoding.ASCII.GetString (digest, 0, n); + } + } + + static bool IsNullOrWhiteSpace (string value) + { + if (string.IsNullOrEmpty (value)) + return true; + + for (int i = 0; i < value.Length; i++) { + if (!char.IsWhiteSpace (value[i])) + return false; + } + + return true; + } + + /// + /// Verifies the Content-Md5 value against an independently computed md5sum. + /// + /// + /// Computes the MD5 checksum of the MIME content and compares it with the + /// value in the Content-MD5 header, returning true if and only if + /// the values match. + /// + /// true, if content MD5 checksum was verified, false otherwise. + public bool VerifyContentMd5 () + { + if (IsNullOrWhiteSpace (md5sum) || Content == null) + return false; + + return md5sum == ComputeContentMd5 (); + } + + /// + /// Prepare the MIME entity for transport using the specified encoding constraints. + /// + /// + /// Prepares the MIME entity for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public override void Prepare (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + switch (ContentTransferEncoding) { + case ContentEncoding.QuotedPrintable: + case ContentEncoding.UUEncode: + case ContentEncoding.Base64: + // these are all safe no matter what the constraints are + return; + case ContentEncoding.Binary: + if (constraint == EncodingConstraint.None) { + // no need to re-encode anything + return; + } + break; + } + + var best = GetBestEncoding (constraint, maxLineLength); + + if (ContentTransferEncoding == ContentEncoding.Default && best == ContentEncoding.SevenBit) + return; + + ContentTransferEncoding = best; + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the MIME part to the output stream. + /// + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override void WriteTo (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + base.WriteTo (options, stream, contentOnly, cancellationToken); + + if (Content == null) + return; + + var cancellable = stream as ICancellableStream; + + if (Content.Encoding != encoding) { + if (encoding == ContentEncoding.UUEncode) { + var begin = string.Format ("begin 0644 {0}", FileName ?? "unknown"); + var buffer = Encoding.UTF8.GetBytes (begin); + + if (cancellable != null) { + cancellable.Write (buffer, 0, buffer.Length, cancellationToken); + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (buffer, 0, buffer.Length); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + } + + // transcode the content into the desired Content-Transfer-Encoding + using (var filtered = new FilteredStream (stream)) { + filtered.Add (EncoderFilter.Create (encoding)); + + if (encoding != ContentEncoding.Binary) + filtered.Add (options.CreateNewLineFilter (EnsureNewLine)); + + Content.DecodeTo (filtered, cancellationToken); + filtered.Flush (cancellationToken); + } + + if (encoding == ContentEncoding.UUEncode) { + var buffer = Encoding.ASCII.GetBytes ("end"); + + if (cancellable != null) { + cancellable.Write (buffer, 0, buffer.Length, cancellationToken); + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (buffer, 0, buffer.Length); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + } + } else if (encoding == ContentEncoding.Binary) { + // Do not alter binary content. + Content.WriteTo (stream, cancellationToken); + } else if (options.VerifyingSignature && Content.NewLineFormat.HasValue && Content.NewLineFormat.Value == NewLineFormat.Mixed) { + // Allow pass-through of the original parsed content without canonicalization when verifying signatures + // if the content contains a mix of line-endings. + // + // See https://github.com/jstedfast/MimeKit/issues/569 for details. + Content.WriteTo (stream, cancellationToken); + } else { + using (var filtered = new FilteredStream (stream)) { + // Note: if we are writing the top-level MimePart, make sure it ends with a new-line so that + // MimeMessage.WriteTo() *always* ends with a new-line. + filtered.Add (options.CreateNewLineFilter (EnsureNewLine)); + Content.WriteTo (filtered, cancellationToken); + filtered.Flush (cancellationToken); + } + } + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the MIME part to the output stream. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteToAsync (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + await base.WriteToAsync (options, stream, contentOnly, cancellationToken).ConfigureAwait (false); + + if (Content == null) + return; + + var isText = ContentType.IsMimeType ("text", "*") || ContentType.IsMimeType ("message", "*"); + + if (Content.Encoding != encoding) { + if (encoding == ContentEncoding.UUEncode) { + var begin = string.Format ("begin 0644 {0}", FileName ?? "unknown"); + var buffer = Encoding.UTF8.GetBytes (begin); + + await stream.WriteAsync (buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false); + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + } + + // transcode the content into the desired Content-Transfer-Encoding + using (var filtered = new FilteredStream (stream)) { + filtered.Add (EncoderFilter.Create (encoding)); + + if (encoding != ContentEncoding.Binary) + filtered.Add (options.CreateNewLineFilter (EnsureNewLine)); + + await Content.DecodeToAsync (filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + + if (encoding == ContentEncoding.UUEncode) { + var buffer = Encoding.ASCII.GetBytes ("end"); + + await stream.WriteAsync (buffer, 0, buffer.Length, cancellationToken).ConfigureAwait (false); + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + } + } else if (encoding == ContentEncoding.Binary) { + // Do not alter binary content. + await Content.WriteToAsync (stream, cancellationToken).ConfigureAwait (false); + } else if (options.VerifyingSignature && Content.NewLineFormat.HasValue && Content.NewLineFormat.Value == NewLineFormat.Mixed) { + // Allow pass-through of the original parsed content without canonicalization when verifying signatures + // if the content contains a mix of line-endings. + // + // See https://github.com/jstedfast/MimeKit/issues/569 for details. + await Content.WriteToAsync (stream, cancellationToken).ConfigureAwait (false); + } else { + using (var filtered = new FilteredStream (stream)) { + // Note: if we are writing the top-level MimePart, make sure it ends with a new-line so that + // MimeMessage.WriteTo() *always* ends with a new-line. + filtered.Add (options.CreateNewLineFilter (EnsureNewLine)); + await Content.WriteToAsync (filtered, cancellationToken).ConfigureAwait (false); + await filtered.FlushAsync (cancellationToken).ConfigureAwait (false); + } + } + } + + /// + /// Called when the headers change in some way. + /// + /// + /// Updates the , , + /// and properties if the corresponding headers have changed. + /// + /// The type of change. + /// The header being added, changed or removed. + protected override void OnHeadersChanged (HeaderListChangedAction action, Header header) + { + int value; + + base.OnHeadersChanged (action, header); + + switch (action) { + case HeaderListChangedAction.Added: + case HeaderListChangedAction.Changed: + switch (header.Id) { + case HeaderId.ContentTransferEncoding: + MimeUtils.TryParse (header.Value, out encoding); + break; + case HeaderId.ContentDuration: + if (int.TryParse (header.Value, out value)) + duration = value; + else + duration = null; + break; + case HeaderId.ContentMd5: + md5sum = header.Value.Trim (); + break; + } + break; + case HeaderListChangedAction.Removed: + switch (header.Id) { + case HeaderId.ContentTransferEncoding: + encoding = ContentEncoding.Default; + break; + case HeaderId.ContentDuration: + duration = null; + break; + case HeaderId.ContentMd5: + md5sum = null; + break; + } + break; + case HeaderListChangedAction.Cleared: + encoding = ContentEncoding.Default; + duration = null; + md5sum = null; + break; + default: + throw new ArgumentOutOfRangeException (nameof (action)); + } + } + } +} diff --git a/src/MimeKit/MimeTypes.cs b/src/MimeKit/MimeTypes.cs new file mode 100644 index 0000000..957248b --- /dev/null +++ b/src/MimeKit/MimeTypes.cs @@ -0,0 +1,1074 @@ +// +// MimeTypes.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A mapping of file name extensions to the corresponding MIME-type. + /// + /// + /// A mapping of file name extensions to the corresponding MIME-type. + /// + public static class MimeTypes + { + static readonly Dictionary extensions; + static readonly Dictionary mimeTypes; + + static MimeTypes () + { + extensions = LoadExtensions (); + mimeTypes = LoadMimeTypes (); + } + + static Dictionary LoadMimeTypes () + { + return new Dictionary (MimeUtils.OrdinalIgnoreCase) { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".7z", "application/x-7z-compressed" }, + { ".aab", "application/x-authorware-bin" }, + { ".aac", "audio/aac" }, + { ".aam", "application/x-authorware-map" }, + { ".aas", "application/x-authorware-seg" }, + { ".abc", "text/vnd.abc" }, + { ".acgi", "text/html" }, + { ".acx", "application/internet-property-stream" }, + { ".afl", "video/animaflex" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".aim", "application/x-aim" }, + { ".aip", "text/x-audiosoft-intra" }, + { ".ani", "application/x-navi-animation" }, + { ".aos", "application/x-nokia-9000-communicator-add-on-software" }, + { ".appcache", "text/cache-manifest" }, + { ".application", "application/x-ms-application" }, + { ".aps", "application/mime" }, + { ".art", "image/x-jg" }, + { ".asf", "video/x-ms-asf" }, + { ".asm", "text/x-asm" }, + { ".asp", "text/asp" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "application/x-mplayer2" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/x-au" }, + { ".avi", "video/avi" }, + { ".avs", "video/avs-video" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bm", "image/bmp" }, + { ".bmp", "image/bmp" }, + { ".boo", "application/book" }, + { ".book", "application/book" }, + { ".boz", "application/x-bzip2" }, + { ".bsh", "application/x-bsh" }, + { ".bz2", "application/x-bzip2" }, + { ".bz", "application/x-bzip" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".ccad", "application/clariscad" }, + { ".cco", "application/x-cocoa" }, + { ".cc", "text/plain" }, + { ".cdf", "application/cdf" }, + { ".cer", "application/pkix-cert" }, + { ".cha", "application/x-chat" }, + { ".chat", "application/x-chat" }, + { ".chm", "application/vnd.ms-htmlhelp" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cod", "image/cis-cod" }, + { ".coffee", "text/x-coffeescript" }, + { ".conf", "text/plain" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".cpt", "application/x-cpt" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/pkix-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "text/csv" }, + { ".cs", "text/plain" }, + { ".c", "text/plain" }, + { ".c++", "text/plain" }, + { ".cxx", "text/plain" }, + { ".dart", "application/dart" }, + { ".dcr", "application/x-director" }, + { ".deb", "application/x-deb" }, + { ".deepv", "application/x-deepv" }, + { ".def", "text/plain" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dif", "video/x-dv" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dll", "application/x-msdownload" }, + { ".dl", "video/dl" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dp", "application/commonground" }, + { ".drw", "application/drafting" }, + { ".dtd", "application/xml-dtd" }, + { ".dvi", "application/x-dvi" }, + { ".dv", "video/x-dv" }, + { ".dwg", "application/acad" }, + { ".dxf", "application/dxf" }, + { ".dxr", "application/x-director" }, + { ".el", "text/x-script.elisp" }, + { ".elc", "application/x-elc" }, + { ".eml", "message/rfc822" }, + { ".eot", "application/vnd.bw-fontobject" }, + { ".eps", "application/postscript" }, + { ".epub", "application/epub+zip" }, + { ".es", "application/x-esrehber" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".exe", "application/octet-stream" }, + { ".f77", "text/plain" }, + { ".f90", "text/plain" }, + { ".fdf", "application/vnd.fdf" }, + { ".fif", "image/fif" }, + { ".flac", "audio/x-flac" }, + { ".fli", "video/fli" }, + { ".flx", "text/vnd.fmi.flexstor" }, + { ".fmf", "video/x-atomic3d-feature" }, + { ".for", "text/plain" }, + { ".fpx", "image/vnd.fpx" }, + { ".frl", "application/freeloader" }, + { ".fsx", "application/fsharp-script" }, + { ".g3", "image/g3fax" }, + { ".gif", "image/gif" }, + { ".gl", "video/gl" }, + { ".gsd", "audio/x-gsm" }, + { ".gsm", "audio/x-gsm" }, + { ".gsp", "application/x-gsp" }, + { ".gss", "application/x-gss" }, + { ".gtar", "application/x-gtar" }, + { ".g", "text/plain" }, + { ".gz", "application/x-gzip" }, + { ".gzip", "application/x-gzip" }, + { ".hdf", "application/x-hdf" }, + { ".help", "application/x-helpfile" }, + { ".hgl", "application/vnd.hp-HPGL" }, + { ".hh", "text/plain" }, + { ".hlb", "text/x-script" }, + { ".hlp", "application/x-helpfile" }, + { ".hpg", "application/vnd.hp-HPGL" }, + { ".hpgl", "application/vnd.hp-HPGL" }, + { ".hqx", "application/binhex" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".h", "text/plain" }, + { ".htmls", "text/html" }, + { ".html", "text/html" }, + { ".htm", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".htx", "text/html" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".idc", "text/plain" }, + { ".ief", "image/ief" }, + { ".iefs", "image/ief" }, + { ".iges", "model/iges" }, + { ".igs", "model/iges" }, + { ".iii", "application/x-iphone" }, + { ".ima", "application/x-ima" }, + { ".imap", "application/x-httpd-imap" }, + { ".inf", "application/inf" }, + { ".ins", "application/x-internett-signup" }, + { ".ip", "application/x-ip2" }, + { ".isp", "application/x-internet-signup" }, + { ".isu", "video/x-isvideo" }, + { ".it", "audio/it" }, + { ".iv", "application/x-inventor" }, + { ".ivf", "video/x-ivf" }, + { ".ivy", "application/x-livescreen" }, + { ".jam", "audio/x-jam" }, + { ".jar", "application/java-archive" }, + { ".java", "text/plain" }, + { ".jav", "text/plain" }, + { ".jcm", "application/x-java-commerce" }, + { ".jfif", "image/jpeg" }, + { ".jfif-tbnl", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpe", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".jps", "image/x-jps" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".jut", "image/jutvision" }, + { ".kar", "audio/midi" }, + { ".ksh", "text/x-script.ksh" }, + { ".la", "audio/nspaudio" }, + { ".lam", "audio/x-liveaudio" }, + { ".latex", "application/x-latex" }, + { ".list", "text/plain" }, + { ".lma", "audio/nspaudio" }, + { ".log", "text/plain" }, + { ".lsp", "application/x-lisp" }, + { ".lst", "text/plain" }, + { ".lsx", "text/x-la-asf" }, + { ".ltx", "application/x-latex" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2a", "audio/mpeg" }, + { ".m2v", "video/mpeg" }, + { ".m3u", "audio/x-mpequrl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "application/x-navimap" }, + { ".mar", "text/plain" }, + { ".markdown", "text/markdown" }, + { ".mbd", "application/mbedlet" }, + { ".mc$", "application/x-magic-cap-package-1.0" }, + { ".mcd", "application/mcad" }, + { ".mcf", "image/vasa" }, + { ".mcp", "application/netmc" }, + { ".md", "text/markdown" }, + { ".mdb", "application/x-msaccess" }, + { ".mesh", "model/mesh" }, + { ".me", "application/x-troff-me" }, + { ".mid", "audio/midi" }, + { ".midi", "audio/midi" }, + { ".mif", "application/x-mif" }, + { ".mjf", "audio/x-vnd.AudioExplosion.MjuiceMediaFile" }, + { ".mjpg", "video/x-motion-jpeg" }, + { ".mm", "application/base64" }, + { ".mme", "application/base64" }, + { ".mny", "application/x-msmoney" }, + { ".mod", "audio/mod" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4a", "audio/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "audio/mpeg" }, + { ".mpc", "application/x-project" }, + { ".mpeg", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpga", "audio/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpt", "application/x-project" }, + { ".mpv2", "video/mpeg" }, + { ".mpv", "application/x-project" }, + { ".mpx", "application/x-project" }, + { ".mrc", "application/marc" }, + { ".ms", "application/x-troff-ms" }, + { ".msh", "model/mesh" }, + { ".m", "text/plain" }, + { ".mvb", "application/x-msmediaview" }, + { ".mv", "video/x-sgi-movie" }, + { ".mzz", "application/x-vnd.AudioExplosion.mzz" }, + { ".nap", "image/naplps" }, + { ".naplps", "image/naplps" }, + { ".nc", "application/x-netcdf" }, + { ".ncm", "application/vnd.nokia.configuration-message" }, + { ".niff", "image/x-niff" }, + { ".nif", "image/x-niff" }, + { ".nix", "application/x-mix-transfer" }, + { ".nsc", "application/x-conference" }, + { ".nvd", "application/x-navidoc" }, + { ".nws", "message/rfc822" }, + { ".oda", "application/oda" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "audio/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".omc", "application/x-omc" }, + { ".omcd", "application/x-omcdatamaker" }, + { ".omcr", "application/x-omcregerator" }, + { ".opus", "audio/ogg" }, + { ".otf", "font/otf" }, + { ".oxps", "application/oxps" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/pkcs-12" }, + { ".p7a", "application/x-pkcs7-signature" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".part", "application/pro_eng" }, + { ".pas", "text/pascal" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcl", "application/x-pcl" }, + { ".pct", "image/x-pict" }, + { ".pcx", "image/x-pcx" }, + { ".pdf", "application/pdf" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pic", "image/pict" }, + { ".pict", "image/pict" }, + { ".pkg", "application/x-newton-compatible-pkg" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pl", "text/plain" }, + { ".plx", "application/x-PiXCLscript" }, + { ".pm4", "application/x-pagemaker" }, + { ".pm5", "application/x-pagemaker" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pm", "image/x-xpixmap" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "application/x-portable-anymap" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".pov", "model/x-pov" }, + { ".ppa", "application/vnd.ms-powerpoint" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".ppz", "application/mspowerpoint" }, + { ".pre", "application/x-freelance" }, + { ".prf", "application/pics-rules" }, + { ".prt", "application/pro_eng" }, + { ".ps", "application/postscript" }, + { ".p", "text/x-pascal" }, + { ".pub", "application/x-mspublisher" }, + { ".pwz", "application/vnd.ms-powerpoint" }, + { ".pyc", "application/x-bytecode.python" }, + { ".py", "text/x-script.phyton" }, + { ".qcp", "audio/vnd.qcelp" }, + { ".qif", "image/x-quicktime" }, + { ".qtc", "video/x-qtc" }, + { ".qtif", "image/x-quicktime" }, + { ".qti", "image/x-quicktime" }, + { ".qt", "video/quicktime" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".ras", "application/x-cmu-raster" }, + { ".rast", "image/cmu-raster" }, + { ".rexx", "text/x-script.rexx" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".rmm", "audio/x-pn-realaudio" }, + { ".rmp", "audio/x-pn-realaudio" }, + { ".rng", "application/ringing-tones" }, + { ".rnx", "application/vnd.rn-realplayer" }, + { ".roff", "application/x-troff" }, + { ".rp", "image/vnd.rn-realpix" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rss", "application/rss+xml" }, + { ".rtf", "text/rtf" }, + { ".rt", "text/richtext" }, + { ".rtx", "text/richtext" }, + { ".rv", "video/vnd.rn-realvideo" }, + { ".s3m", "audio/s3m" }, + { ".sbk", "application/x-tbook" }, + { ".scd", "application/x-msschedule" }, + { ".scm", "application/x-lotusscreencam" }, + { ".sct", "text/scriptlet" }, + { ".sdml", "text/plain" }, + { ".sdp", "application/sdp" }, + { ".sdr", "application/sounder" }, + { ".sea", "application/sea" }, + { ".set", "application/set" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sgm", "text/sgml" }, + { ".shar", "application/x-bsh" }, + { ".sh", "text/x-script.sh" }, + { ".shtml", "text/html" }, + { ".sid", "audio/x-psid" }, + { ".silo", "model/mesh" }, + { ".sit", "application/x-sit" }, + { ".skd", "application/x-koan" }, + { ".skm", "application/x-koan" }, + { ".skp", "application/x-koan" }, + { ".skt", "application/x-koan" }, + { ".sl", "application/x-seelogo" }, + { ".smi", "application/smil" }, + { ".smil", "application/smil" }, + { ".snd", "audio/basic" }, + { ".sol", "application/solids" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spr", "application/x-sprite" }, + { ".sprite", "application/x-sprite" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssi", "text/x-server-parsed-html" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".step", "application/step" }, + { ".s", "text/x-asm" }, + { ".stl", "application/sla" }, + { ".stm", "text/html" }, + { ".stp", "application/step" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svf", "image/x-dwg" }, + { ".svg", "image/svg+xml" }, + { ".svr", "application/x-world" }, + { ".swf", "application/x-shockwave-flash" }, + { ".talk", "text/x-speech" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tbk", "application/toolbook" }, + { ".tcl", "text/x-script.tcl" }, + { ".tcsh", "text/x-script.tcsh" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".text", "text/plain" }, + { ".tgz", "application/x-compressed" }, + { ".tiff", "image/tiff" }, + { ".tif", "image/tiff" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "application/typescript" }, + { ".tsi", "audio/tsp-audio" }, + { ".tsp", "audio/tsplayer" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttc", "font/collection" }, + { ".ttf", "font/ttf" }, + { ".txt", "text/plain" }, + { ".uil", "text/x-uil" }, + { ".uls", "text/iuls" }, + { ".unis", "text/uri-list" }, + { ".uni", "text/uri-list" }, + { ".unv", "application/i-deas" }, + { ".uris", "text/uri-list" }, + { ".uri", "text/uri-list" }, + { ".ustar", "multipart/x-ustar" }, + { ".uue", "text/x-uuencode" }, + { ".uu", "text/x-uuencode" }, + { ".vcd", "application/x-cdlink" }, + { ".vcf", "text/vcard" }, + { ".vcard", "text/vcard" }, + { ".vcs", "text/x-vcalendar" }, + { ".vda", "application/vda" }, + { ".vdo", "video/vdo" }, + { ".vew", "application/groupwise" }, + { ".vivo", "video/vnd.vivo" }, + { ".viv", "video/vnd.vivo" }, + { ".vmd", "application/vocaltec-media-desc" }, + { ".vmf", "application/vocaltec-media-file" }, + { ".voc", "audio/voc" }, + { ".vos", "video/vosaic" }, + { ".vox", "audio/voxware" }, + { ".vqe", "audio/x-twinvq-plugin" }, + { ".vqf", "audio/x-twinvq" }, + { ".vql", "audio/x-twinvq-plugin" }, + { ".vrml", "application/x-vrml" }, + { ".vsd", "application/x-visio" }, + { ".vst", "application/x-visio" }, + { ".vsw", "application/x-visio" }, + { ".w60", "application/wordperfect6.0" }, + { ".w61", "application/wordperfect6.1" }, + { ".w6w", "application/msword" }, + { ".wav", "audio/wav" }, + { ".wb1", "application/x-qpro" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".web", "application/vnd.xara" }, + { ".weba", "audio/webm" }, + { ".webm", "video/webm" }, + { ".webp", "image/webp" }, + { ".wiz", "application/msword" }, + { ".wk1", "application/x-123" }, + { ".wks", "application/vnd.ms-works" }, + { ".wmf", "image/wmf" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".woff", "font/woff" }, + { ".woff2", "font/woff2" }, + { ".word", "application/msword" }, + { ".wp5", "application/wordperfect" }, + { ".wp6", "application/wordperfect" }, + { ".wp", "application/wordperfect" }, + { ".wpd", "application/wordperfect" }, + { ".wps", "application/vnd.ms-works" }, + { ".wq1", "application/x-lotus" }, + { ".wri", "application/mswrite" }, + { ".wrl", "application/x-world" }, + { ".wrz", "model/vrml" }, + { ".wsc", "text/scriplet" }, + { ".wsdl", "text/xml" }, + { ".wsrc", "application/x-wais-source" }, + { ".wtk", "application/x-wintalk" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x3d", "model/x3d+xml" }, + { ".x3db", "model/x3d+fastinfoset" }, + { ".x3dv", "model/x3d-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "video/x-amt-demorun" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xif", "image/vnd.xiff" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xl", "application/excel" }, + { ".xlb", "application/excel" }, + { ".xlc", "application/excel" }, + { ".xld", "application/excel" }, + { ".xlk", "application/excel" }, + { ".xll", "application/excel" }, + { ".xlm", "application/excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlv", "application/excel" }, + { ".xlw", "application/excel" }, + { ".xm", "audio/xm" }, + { ".xml", "text/xml" }, + { ".xpi", "application/x-xpinstall" }, + { ".xpix", "application/x-vnd.ls-xpix" }, + { ".xpm", "image/xpm" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".x-png", "image/png" }, + { ".xsd", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsr", "video/x-amt-showrun" }, + { ".xwd", "image/x-xwd" }, + { ".z", "application/x-compressed" }, + { ".zip", "application/zip" }, + { ".zsh", "text/x-script.zsh" } + }; + } + + static Dictionary LoadExtensions () + { + return new Dictionary (MimeUtils.OrdinalIgnoreCase) { + { "application/acad", ".dwg" }, + { "application/atom+xml", ".atom" }, + { "application/base64", ".mm" }, + { "application/binhex", ".hqx" }, + { "application/book", ".boo" }, + { "application/cdf", ".cdf" }, + { "application/clariscad", ".ccad" }, + { "application/commonground", ".dp" }, + { "application/dart", ".dart" }, + { "application/drafting", ".drw" }, + { "application/dxf", ".dxf" }, + { "application/envoy", ".evy" }, + { "application/epub+zip", ".epub" }, + { "application/excel", ".xls" }, + { "application/freeloader", ".frl" }, + { "application/fsharp-script", ".fsx" }, + { "application/futuresplash", ".spl" }, + { "application/groupwise", ".vew" }, + { "application/hta", ".hta" }, + { "application/i-deas", ".unv" }, + { "application/inf", ".inf" }, + { "application/internet-property-stream", ".acx" }, + { "application/java-archive", ".jar" }, + { "application/javascript", ".js" }, + { "application/json", ".json" }, + { "application/marc", ".mrc" }, + { "application/mbedlet", ".mbd" }, + { "application/mcad", ".mcd" }, + { "application/mime", ".aps" }, + { "application/mspowerpoint", ".ppz" }, + { "application/msword", ".doc" }, + { "application/mswrite", ".wri" }, + { "application/netmc", ".mcp" }, + { "application/octet-stream", ".bin" }, + { "application/oda", ".oda" }, + { "application/ogg", ".ogx" }, + { "application/oleobject", ".ods" }, + { "application/olescript", ".axs" }, + { "application/oxps", ".oxps" }, + { "application/pdf", ".pdf" }, + { "application/pics-rules", ".prf" }, + { "application/pkcs-12", ".p12" }, + { "application/pkcs10", ".p10" }, + { "application/pkcs7-mime", ".p7m" }, + { "application/pkcs7-signature", ".p7s" }, + { "application/pkix-cert", ".cer" }, + { "application/pkix-crl", ".crl" }, + { "application/postscript", ".ps" }, + { "application/pro_eng", ".part" }, + { "application/ringing-tones", ".rng" }, + { "application/rss+xml", ".rss" }, + { "application/sdp", ".sdp" }, + { "application/sea", ".sea" }, + { "application/set", ".set" }, + { "application/set-payment-initiation", ".setpay" }, + { "application/set-registration-initiation", ".setreg" }, + { "application/sla", ".stl" }, + { "application/smil", ".smi" }, + { "application/solids", ".sol" }, + { "application/sounder", ".sdr" }, + { "application/step", ".step" }, + { "application/streamingmedia", ".ssm" }, + { "application/toolbook", ".tbk" }, + { "application/typescript", ".ts" }, + { "application/vda", ".vda" }, + { "application/vnd.bw-fontobject", ".eot" }, + { "application/vnd.fdf", ".fdf" }, + { "application/vnd.hp-HPGL", ".hgl" }, + { "application/vnd.ms-excel", ".xls" }, + { "application/vnd.ms-excel.addin.macroEnabled.12", ".xlam" }, + { "application/vnd.ms-excel.sheet.binary.macroEnabled.12", ".xlsb" }, + { "application/vnd.ms-excel.sheet.macroEnabled.12", ".xlsm" }, + { "application/vnd.ms-excel.template.macroEnabled.12", ".xltm" }, + { "application/vnd.ms-htmlhelp", ".chm" }, + { "application/vnd.ms-pki.certstore", ".sst" }, + { "application/vnd.ms-pki.pko", ".pko" }, + { "application/vnd.ms-pki.seccat", ".cat" }, + { "application/vnd.ms-powerpoint", ".ppt" }, + { "application/vnd.ms-powerpoint.addin.macroEnabled.12", ".ppam" }, + { "application/vnd.ms-powerpoint.presentation.macroEnabled.12", ".pptm" }, + { "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", ".ppsm" }, + { "application/vnd.ms-powerpoint.template.macroEnabled.12", ".potm" }, + { "application/vnd.ms-project", ".mpp" }, + { "application/vnd.ms-word.document.macroEnabled.12", ".docm" }, + { "application/vnd.ms-word.template.macroEnabled.12", ".dotm" }, + { "application/vnd.ms-works", ".wcm" }, + { "application/vnd.ms-xpsdocument", ".xps" }, + { "application/vnd.nokia.configuration-message", ".ncm" }, + { "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx" }, + { "application/vnd.openxmlformats-officedocument.presentationml.slideshow", ".ppsx" }, + { "application/vnd.openxmlformats-officedocument.presentationml.template", ".potx" }, + { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx" }, + { "application/vnd.openxmlformats-officedocument.spreadsheetml.template", ".xltx" }, + { "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx" }, + { "application/vnd.openxmlformats-officedocument.wordprocessingml.template", ".dotx" }, + { "application/vnd.rn-realmedia", ".rm" }, + { "application/vnd.rn-realplayer", ".rnx" }, + { "application/vnd.wap.wmlc", ".wmlc" }, + { "application/vnd.wap.wmlscriptc", ".wmlsc" }, + { "application/vnd.xara", ".web" }, + { "application/vocaltec-media-desc", ".vmd" }, + { "application/vocaltec-media-file", ".vmf" }, + { "application/wordperfect", ".wp5" }, + { "application/wordperfect6.0", ".w60" }, + { "application/wordperfect6.1", ".w61" }, + { "application/x-123", ".wk1" }, + { "application/x-7z-compressed", ".7z" }, + { "application/x-aim", ".aim" }, + { "application/x-authorware-bin", ".aab" }, + { "application/x-authorware-map", ".aam" }, + { "application/x-authorware-seg", ".aas" }, + { "application/x-bcpio", ".bcpio" }, + { "application/x-bsh", ".bsh" }, + { "application/x-bytecode.python", ".pyc" }, + { "application/x-bzip", ".bz" }, + { "application/x-bzip2", ".bz2" }, + { "application/x-cdlink", ".vcd" }, + { "application/x-chat", ".cha" }, + { "application/x-cmu-raster", ".ras" }, + { "application/x-cocoa", ".cco" }, + { "application/x-compressed", ".z" }, + { "application/x-conference", ".nsc" }, + { "application/x-cpio", ".cpio" }, + { "application/x-cpt", ".cpt" }, + { "application/x-csh", ".csh" }, + { "application/x-deb", ".deb" }, + { "application/x-deepv", ".deepv" }, + { "application/x-director", ".dcr" }, + { "application/x-dvi", ".dvi" }, + { "application/x-elc", ".elc" }, + { "application/x-esrehber", ".es" }, + { "application/x-font-ttf", ".ttf" }, + { "application/x-freelance", ".pre" }, + { "application/x-gsp", ".gsp" }, + { "application/x-gss", ".gss" }, + { "application/x-gtar", ".gtar" }, + { "application/x-gzip", ".gz" }, + { "application/x-hdf", ".hdf" }, + { "application/x-helpfile", ".help" }, + { "application/x-httpd-imap", ".imap" }, + { "application/x-ima", ".ima" }, + { "application/x-internet-signup", ".isp" }, + { "application/x-internett-signup", ".ins" }, + { "application/x-inventor", ".iv" }, + { "application/x-ip2", ".ip" }, + { "application/x-iphone", ".iii" }, + { "application/x-java-applet", ".class" }, + { "application/x-java-commerce", ".jcm" }, + { "application/x-koan", ".skd" }, + { "application/x-latex", ".latex" }, + { "application/x-lisp", ".lsp" }, + { "application/x-livescreen", ".ivy" }, + { "application/x-lotus", ".wq1" }, + { "application/x-lotusscreencam", ".scm" }, + { "application/x-magic-cap-package-1.0", ".mc$" }, + { "application/x-mif", ".mif" }, + { "application/x-mix-transfer", ".nix" }, + { "application/x-mplayer2", ".asx" }, + { "application/x-ms-application", ".application" }, + { "application/x-ms-manifest", ".manifest" }, + { "application/x-ms-xbap", ".xbap" }, + { "application/x-msaccess", ".mdb" }, + { "application/x-mscardfile", ".crd" }, + { "application/x-msclip", ".clp" }, + { "application/x-msdownload", ".dll" }, + { "application/x-msmediaview", ".m13" }, + { "application/x-msmoney", ".mny" }, + { "application/x-mspublisher", ".pub" }, + { "application/x-msschedule", ".scd" }, + { "application/x-msterminal", ".trm" }, + { "application/x-navi-animation", ".ani" }, + { "application/x-navidoc", ".nvd" }, + { "application/x-navimap", ".map" }, + { "application/x-netcdf", ".nc" }, + { "application/x-newton-compatible-pkg", ".pkg" }, + { "application/x-nokia-9000-communicator-add-on-software", ".aos" }, + { "application/x-omc", ".omc" }, + { "application/x-omcdatamaker", ".omcd" }, + { "application/x-omcregerator", ".omcr" }, + { "application/x-pagemaker", ".pm4" }, + { "application/x-pcl", ".pcl" }, + { "application/x-perfmon", ".pma" }, + { "application/x-PiXCLscript", ".plx" }, + { "application/x-pkcs12", ".pfx" }, + { "application/x-pkcs7-certificates", ".p7b" }, + { "application/x-pkcs7-certreqresp", ".p7r" }, + { "application/x-pkcs7-signature", ".p7a" }, + { "application/x-portable-anymap", ".pnm" }, + { "application/x-project", ".mpc" }, + { "application/x-qpro", ".wb1" }, + { "application/x-seelogo", ".sl" }, + { "application/x-shockwave-flash", ".swf" }, + { "application/x-silverlight-app", ".xap" }, + { "application/x-sit", ".sit" }, + { "application/x-sprite", ".spr" }, + { "application/x-sv4cpio", ".sv4cpio" }, + { "application/x-sv4crc", ".sv4crc" }, + { "application/x-tar", ".tar" }, + { "application/x-tbook", ".sbk" }, + { "application/x-tex", ".tex" }, + { "application/x-texinfo", ".texi" }, + { "application/x-troff", ".roff" }, + { "application/x-troff-man", ".man" }, + { "application/x-troff-me", ".me" }, + { "application/x-troff-ms", ".ms" }, + { "application/x-visio", ".vsd" }, + { "application/x-vnd.AudioExplosion.mzz", ".mzz" }, + { "application/x-vnd.ls-xpix", ".xpix" }, + { "application/x-vrml", ".vrml" }, + { "application/x-wais-source", ".src" }, + { "application/x-wintalk", ".wtk" }, + { "application/x-woff", ".woff" }, + { "application/x-world", ".svr" }, + { "application/x-x509-ca-cert", ".der" }, + { "application/x-xpinstall", ".xpi" }, + { "application/xaml+xml", ".xaml" }, + { "application/xhtml+xml", ".xhtml" }, + { "application/xml", ".xml" }, + { "application/xml-dtd", ".dtd" }, + { "application/zip", ".zip" }, + { "audio/aac", ".aac" }, + { "audio/aiff", ".aiff" }, + { "audio/basic", ".snd" }, + { "audio/it", ".it" }, + { "audio/mid", ".rmi" }, + { "audio/midi", ".mid" }, + { "audio/mod", ".mod" }, + { "audio/mp4", ".mp4" }, + { "audio/mpeg", ".mp3" }, + { "audio/nspaudio", ".la" }, + { "audio/ogg", ".ogg" }, + { "audio/s3m", ".s3m" }, + { "audio/tsp-audio", ".tsi" }, + { "audio/tsplayer", ".tsp" }, + { "audio/vnd.qcelp", ".qcp" }, + { "audio/voc", ".voc" }, + { "audio/vorbis", ".ogg" }, + { "audio/voxware", ".vox" }, + { "audio/wav", ".wav" }, + { "audio/webm", ".weba" }, + { "audio/x-au", ".au" }, + { "audio/x-flac", ".flac" }, + { "audio/x-gsm", ".gsd" }, + { "audio/x-jam", ".jam" }, + { "audio/x-liveaudio", ".lam" }, + { "audio/x-mpequrl", ".m3u" }, + { "audio/x-pn-realaudio", ".ra" }, + { "audio/x-pn-realaudio-plugin", ".rpm" }, + { "audio/x-psid", ".sid" }, + { "audio/x-twinvq", ".vqf" }, + { "audio/x-twinvq-plugin", ".vqe" }, + { "audio/x-vnd.AudioExplosion.MjuiceMediaFile", ".mjf" }, + { "audio/xm", ".xm" }, + { "font/collection", ".ttc" }, + { "font/otf", ".otf" }, + { "font/sfnt", ".ttf" }, + { "font/ttf", ".ttf" }, + { "font/woff", ".woff" }, + { "font/woff2", ".woff2" }, + { "image/bmp", ".bmp" }, + { "image/cis-cod", ".cod" }, + { "image/cmu-raster", ".rast" }, + { "image/fif", ".fif" }, + { "image/g3fax", ".g3" }, + { "image/gif", ".gif" }, + { "image/ief", ".ief" }, + { "image/jpeg", ".jpg" }, + { "image/jutvision", ".jut" }, + { "image/naplps", ".nap" }, + { "image/pict", ".pic" }, + { "image/png", ".png" }, + { "image/svg+xml", ".svg" }, + { "image/tiff", ".tif" }, + { "image/vasa", ".mcf" }, + { "image/vnd.fpx", ".fpx" }, + { "image/vnd.rn-realflash", ".rf" }, + { "image/vnd.rn-realpix", ".rp" }, + { "image/vnd.wap.wbmp", ".wbmp" }, + { "image/vnd.xiff", ".xif" }, + { "image/webp", ".webp" }, + { "image/wmf", ".wmf" }, + { "image/x-cmx", ".cmx" }, + { "image/x-dwg", ".svf" }, + { "image/x-icon", ".ico" }, + { "image/x-jg", ".art" }, + { "image/x-jps", ".jps" }, + { "image/x-niff", ".niff" }, + { "image/x-pcx", ".pcx" }, + { "image/x-pict", ".pct" }, + { "image/x-png", ".png" }, + { "image/x-portable-bitmap", ".pbm" }, + { "image/x-portable-graymap", ".pgm" }, + { "image/x-portable-pixmap", ".ppm" }, + { "image/x-quicktime", ".qif" }, + { "image/x-rgb", ".rgb" }, + { "image/x-xbitmap", ".xbm" }, + { "image/x-xpixmap", ".pm" }, + { "image/x-xwd", ".xwd" }, + { "image/xpm", ".xpm" }, + { "message/rfc822", ".eml" }, + { "model/iges", ".iges" }, + { "model/mesh", ".mesh" }, + { "model/vrml", ".wrz" }, + { "model/x-pov", ".pov" }, + { "model/x3d+fastinfoset", ".x3db" }, + { "model/x3d+xml", ".x3d" }, + { "model/x3d-vrml", ".x3dv" }, + { "multipart/x-ustar", ".ustar" }, + { "text/asp", ".asp" }, + { "text/cache-manifest", ".appcache" }, + { "text/calendar", ".ics" }, + { "text/css", ".css" }, + { "text/csv", ".csv" }, + { "text/h323", ".323" }, + { "text/html", ".html" }, + { "text/iuls", ".uls" }, + { "text/markdown", ".md" }, + { "text/pascal", ".pas" }, + { "text/plain", ".txt" }, + { "text/richtext", ".rtx" }, + { "text/rtf", ".rtf" }, + { "text/scriplet", ".wsc" }, + { "text/scriptlet", ".sct" }, + { "text/sgml", ".sgml" }, + { "text/tab-separated-values", ".tsv" }, + { "text/uri-list", ".uri" }, + { "text/vcard", ".vcf" }, + { "text/vnd.abc", ".abc" }, + { "text/vnd.fmi.flexstor", ".flx" }, + { "text/vnd.wap.wml", ".wml" }, + { "text/vnd.wap.wmlscript", ".wmls" }, + { "text/webviewhtml", ".htt" }, + { "text/x-asm", ".asm" }, + { "text/x-audiosoft-intra", ".aip" }, + { "text/x-coffeescript", ".coffee" }, + { "text/x-component", ".htc" }, + { "text/x-la-asf", ".lsx" }, + { "text/x-pascal", ".p" }, + { "text/x-script", ".hlb" }, + { "text/x-script.elisp", ".el" }, + { "text/x-script.ksh", ".ksh" }, + { "text/x-script.phyton", ".py" }, + { "text/x-script.rexx", ".rexx" }, + { "text/x-script.sh", ".sh" }, + { "text/x-script.tcl", ".tcl" }, + { "text/x-script.tcsh", ".tcsh" }, + { "text/x-script.zsh", ".zsh" }, + { "text/x-server-parsed-html", ".ssi" }, + { "text/x-setext", ".etx" }, + { "text/x-speech", ".talk" }, + { "text/x-uil", ".uil" }, + { "text/x-uuencode", ".uu" }, + { "text/x-vcalendar", ".vcs" }, + { "text/xml", ".xml" }, + { "video/3gpp", ".3gp" }, + { "video/3gpp2", ".3g2" }, + { "video/animaflex", ".afl" }, + { "video/avi", ".avi" }, + { "video/avs-video", ".avs" }, + { "video/dl", ".dl" }, + { "video/fli", ".fli" }, + { "video/gl", ".gl" }, + { "video/mp4", ".mp4" }, + { "video/mpeg", ".mpg" }, + { "video/ogg", ".ogv" }, + { "video/quicktime", ".mov" }, + { "video/vdo", ".vdo" }, + { "video/vnd.rn-realvideo", ".rv" }, + { "video/vnd.vivo", ".vivo" }, + { "video/vosaic", ".vos" }, + { "video/webm", ".webm" }, + { "video/x-amt-demorun", ".xdr" }, + { "video/x-amt-showrun", ".xsr" }, + { "video/x-atomic3d-feature", ".fmf" }, + { "video/x-dv", ".dif" }, + { "video/x-isvideo", ".isu" }, + { "video/x-ivf", ".ivf" }, + { "video/x-motion-jpeg", ".mjpg" }, + { "video/x-ms-asf", ".asf" }, + { "video/x-ms-wmp", ".wmp" }, + { "video/x-ms-wmv", ".wmv" }, + { "video/x-ms-wmx", ".wmx" }, + { "video/x-ms-wvx", ".wvx" }, + { "video/x-qtc", ".qtc" }, + { "video/x-sgi-movie", ".movie" } + }; + } + + /// + /// Get the MIME-type of a file. + /// + /// + /// Gets the MIME-type of a file based on the file extension. + /// + /// The MIME-type. + /// The file name. + /// + /// is null. + /// + public static string GetMimeType (string fileName) + { + if (fileName == null) + throw new ArgumentNullException (nameof (fileName)); + + var extension = Path.GetExtension (fileName); + + mimeTypes.TryGetValue (extension, out var mimeType); + + return mimeType ?? "application/octet-stream"; + } + + /// + /// Get the standard file extension for a MIME-type. + /// + /// + /// Gets the standard file extension for a MIME-type. + /// + /// true if the extension is known for the specified MIME-type; otherwise, false. + /// The MIME-type. + /// The file name extension for the specified MIME-type. + /// + /// is null. + /// + public static bool TryGetExtension (string mimeType, out string extension) + { + if (mimeType == null) + throw new ArgumentNullException (nameof (mimeType)); + + return extensions.TryGetValue (mimeType, out extension); + } + + /// + /// Register a MIME-type to file extension mapping. + /// + /// + /// Registers a MIME-type to file extension mapping. + /// If the mapping for the MIME-type and/or file extension already exists, + /// then it is overridden by the new mapping. + /// + /// The MIME-type to register. + /// The file extension to register. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is empty. + /// -or- + /// is empty. + /// + public static void Register (string mimeType, string extension) + { + if (mimeType == null) + throw new ArgumentNullException (nameof (mimeType)); + + if (mimeType.Length == 0) + throw new ArgumentException ("Cannot register an empty MIME-type.", nameof (mimeType)); + + if (extension == null) + throw new ArgumentNullException (nameof (extension)); + + if (extension.Length == 0) + throw new ArgumentException ("Cannot register an empty file extension.", nameof (extension)); + + if (extension[0] != '.') + extension = "." + extension; + + mimeTypes[extension] = mimeType; + extensions[mimeType] = extension; + } + } +} diff --git a/src/MimeKit/MimeVisitor.cs b/src/MimeKit/MimeVisitor.cs new file mode 100644 index 0000000..39b2cad --- /dev/null +++ b/src/MimeKit/MimeVisitor.cs @@ -0,0 +1,385 @@ +// +// MimeVisitor.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if ENABLE_CRYPTO +using MimeKit.Cryptography; +#endif + +using MimeKit.Tnef; + +namespace MimeKit { + /// + /// Represents a visitor for MIME trees. + /// + /// + /// This class is designed to be inherited to create more specialized classes whose + /// functionality requires traversing, examining or copying a MIME tree. + /// + /// + /// + /// + public abstract class MimeVisitor + { + /// + /// Dispatches the entity to one of the more specialized visit methods in this class. + /// + /// + /// Dispatches the entity to one of the more specialized visit methods in this class. + /// + /// The MIME entity. + public virtual void Visit (MimeEntity entity) + { + if (entity != null) + entity.Accept (this); + } + + /// + /// Dispatches the message to one of the more specialized visit methods in this class. + /// + /// + /// Dispatches the message to one of the more specialized visit methods in this class. + /// + /// The MIME message. + public virtual void Visit (MimeMessage message) + { + if (message != null) + message.Accept (this); + } + +#if ENABLE_CRYPTO + /// + /// Visit the application/pgp-encrypted MIME entity. + /// + /// + /// Visits the application/pgp-encrypted MIME entity. + /// + /// + /// The application/pgp-encrypted MIME entity. + protected internal virtual void VisitApplicationPgpEncrypted (ApplicationPgpEncrypted entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the application/pgp-signature MIME entity. + /// + /// + /// Visits the application/pgp-signature MIME entity. + /// + /// + /// The application/pgp-signature MIME entity. + protected internal virtual void VisitApplicationPgpSignature (ApplicationPgpSignature entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the application/pkcs7-mime MIME entity. + /// + /// + /// Visits the application/pkcs7-mime MIME entity. + /// + /// The application/pkcs7-mime MIME entity. + protected internal virtual void VisitApplicationPkcs7Mime (ApplicationPkcs7Mime entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the application/pkcs7-signature MIME entity. + /// + /// + /// Visits the application/pkcs7-signature MIME entity. + /// + /// + /// The application/pkcs7-signature MIME entity. + protected internal virtual void VisitApplicationPkcs7Signature (ApplicationPkcs7Signature entity) + { + VisitMimePart (entity); + } +#endif + + /// + /// Visit the message/disposition-notification MIME entity. + /// + /// + /// Visits the message/disposition-notification MIME entity. + /// + /// The message/disposition-notification MIME entity. + protected internal virtual void VisitMessageDispositionNotification (MessageDispositionNotification entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the message/delivery-status MIME entity. + /// + /// + /// Visits the message/delivery-status MIME entity. + /// + /// The message/delivery-status MIME entity. + protected internal virtual void VisitMessageDeliveryStatus (MessageDeliveryStatus entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the message contained within a message/rfc822 or message/news MIME entity. + /// + /// + /// Visits the message contained within a message/rfc822 or message/news MIME entity. + /// + /// The message/rfc822 or message/news MIME entity. + protected virtual void VisitMessage (MessagePart entity) + { + if (entity.Message != null) + entity.Message.Accept (this); + } + + /// + /// Visit the message/rfc822 or message/news MIME entity. + /// + /// + /// Visits the message/rfc822 or message/news MIME entity. + /// + /// + /// + /// + /// The message/rfc822 or message/news MIME entity. + protected internal virtual void VisitMessagePart (MessagePart entity) + { + VisitMimeEntity (entity); + VisitMessage (entity); + } + + /// + /// Visit the message/partial MIME entity. + /// + /// + /// Visits the message/partial MIME entity. + /// + /// The message/partial MIME entity. + protected internal virtual void VisitMessagePartial (MessagePartial entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the abstract MIME entity. + /// + /// + /// Visits the abstract MIME entity. + /// + /// The MIME entity. + protected internal virtual void VisitMimeEntity (MimeEntity entity) + { + } + + /// + /// Visit the body of the message. + /// + /// + /// Visits the body of the message. + /// + /// The message. + protected virtual void VisitBody (MimeMessage message) + { + if (message.Body != null) + message.Body.Accept (this); + } + + /// + /// Visit the MIME message. + /// + /// + /// Visits the MIME message. + /// + /// The MIME message. + protected internal virtual void VisitMimeMessage (MimeMessage message) + { + VisitBody (message); + } + + /// + /// Visit the abstract MIME part entity. + /// + /// + /// Visits the MIME part entity. + /// + /// + /// + /// + /// The MIME part entity. + protected internal virtual void VisitMimePart (MimePart entity) + { + VisitMimeEntity (entity); + } + + /// + /// Visit the children of a . + /// + /// + /// Visits the children of a . + /// + /// Multipart. + protected virtual void VisitChildren (Multipart multipart) + { + for (int i = 0; i < multipart.Count; i++) + multipart[i].Accept (this); + } + + /// + /// Visit the abstract multipart MIME entity. + /// + /// + /// Visits the abstract multipart MIME entity. + /// + /// The multipart MIME entity. + protected internal virtual void VisitMultipart (Multipart multipart) + { + VisitMimeEntity (multipart); + VisitChildren (multipart); + } + + /// + /// Visit the multipart/alternative MIME entity. + /// + /// + /// Visits the multipart/alternative MIME entity. + /// + /// + /// + /// + /// The multipart/alternative MIME entity. + protected internal virtual void VisitMultipartAlternative (MultipartAlternative alternative) + { + VisitMultipart (alternative); + } + +#if ENABLE_CRYPTO + /// + /// Visit the multipart/encrypted MIME entity. + /// + /// + /// Visits the multipart/encrypted MIME entity. + /// + /// The multipart/encrypted MIME entity. + protected internal virtual void VisitMultipartEncrypted (MultipartEncrypted encrypted) + { + VisitMultipart (encrypted); + } +#endif + + /// + /// Visit the multipart/related MIME entity. + /// + /// + /// Visits the multipart/related MIME entity. + /// + /// + /// + /// + /// The multipart/related MIME entity. + protected internal virtual void VisitMultipartRelated (MultipartRelated related) + { + VisitMultipart (related); + } + + /// + /// Visit the multipart/report MIME entity. + /// + /// + /// Visits the multipart/report MIME entity. + /// + /// + /// + /// + /// The multipart/report MIME entity. + protected internal virtual void VisitMultipartReport (MultipartReport report) + { + VisitMultipart (report); + } + +#if ENABLE_CRYPTO + /// + /// Visit the multipart/signed MIME entity. + /// + /// + /// Visits the multipart/signed MIME entity. + /// + /// The multipart/signed MIME entity. + protected internal virtual void VisitMultipartSigned (MultipartSigned signed) + { + VisitMultipart (signed); + } +#endif + + /// + /// Visit the text-based MIME part entity. + /// + /// + /// Visits the text-based MIME part entity. + /// + /// + /// + /// + /// The text-based MIME part entity. + protected internal virtual void VisitTextPart (TextPart entity) + { + VisitMimePart (entity); + } + + /// + /// Visit the text/rfc822-headers MIME entity. + /// + /// + /// Visits the text/rfc822-headers MIME entity. + /// + /// + /// + /// + /// The text/rfc822-headers MIME entity. + protected internal virtual void VisitTextRfc822Headers (TextRfc822Headers entity) + { + VisitMessagePart (entity); + } + + /// + /// Visit the Microsoft TNEF MIME part entity. + /// + /// + /// Visits the Microsoft TNEF MIME part entity. + /// + /// + /// + /// + /// The Microsoft TNEF MIME part entity. + protected internal virtual void VisitTnefPart (TnefPart entity) + { + VisitMimePart (entity); + } + } +} diff --git a/src/MimeKit/Multipart.cs b/src/MimeKit/Multipart.cs new file mode 100644 index 0000000..976f1ed --- /dev/null +++ b/src/MimeKit/Multipart.cs @@ -0,0 +1,828 @@ +// +// Multipart.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using MimeKit.Encodings; +using MimeKit.Utils; +using MimeKit.IO; + +namespace MimeKit { + /// + /// A multipart MIME entity which may contain a collection of MIME entities. + /// + /// + /// All multipart MIME entities will have a Content-Type with a media type of "multipart". + /// The most common multipart MIME entity used in email is the "multipart/mixed" entity. + /// Four (4) initial subtypes were defined in the original MIME specifications: mixed, alternative, + /// digest, and parallel. + /// The "multipart/mixed" type is a sort of general-purpose container. When used in email, the + /// first entity is typically the "body" of the message while additional entities are most often + /// file attachments. + /// Speaking of message "bodies", the "multipart/alternative" type is used to offer a list of + /// alternative formats for the main body of the message (usually they will be "text/plain" and + /// "text/html"). These alternatives are in order of increasing faithfulness to the original document + /// (in other words, the last entity will be in a format that, when rendered, will most closely match + /// what the sending client's WYSISYG editor produced). + /// The "multipart/digest" type will typically contain a digest of MIME messages and is most + /// commonly used by mailing-list software. + /// The "multipart/parallel" type contains entities that are all meant to be shown (or heard) + /// in parallel. + /// Another commonly used type is the "multipart/related" type which contains, as one might expect, + /// inter-related MIME parts which typically reference each other via URIs based on the Content-Id and/or + /// Content-Location headers. + /// + public class Multipart : MimeEntity, ICollection, IList + { + readonly List children; + string preamble, epilogue; + + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public Multipart (MimeEntityConstructorArgs args) : base (args) + { + children = new List (); + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified subtype. + /// + /// The multipart media sub-type. + /// An array of initialization parameters: headers and MIME entities. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains one or more arguments of an unknown type. + /// + public Multipart (string subtype, params object[] args) : this (subtype) + { + if (args == null) + throw new ArgumentNullException (nameof (args)); + + foreach (object obj in args) { + if (obj == null || TryInit (obj)) + continue; + + if (obj is MimeEntity entity) { + Add (entity); + continue; + } + + throw new ArgumentException ("Unknown initialization parameter: " + obj.GetType ()); + } + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with the specified subtype. + /// + /// The multipart media sub-type. + /// + /// is null. + /// + public Multipart (string subtype) : base ("multipart", subtype) + { + ContentType.Boundary = GenerateBoundary (); + children = new List (); + WriteEndBoundary = true; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new with a ContentType of multipart/mixed. + /// + public Multipart () : this ("mixed") + { + } + + static string GenerateBoundary () + { + var base64 = new Base64Encoder (true); + var digest = new byte[16]; + var buf = new byte[24]; + int length; + + MimeUtils.GetRandomBytes (digest); + + length = base64.Flush (digest, 0, digest.Length, buf); + + return "=-" + Encoding.ASCII.GetString (buf, 0, length); + } + + /// + /// Get or set the boundary. + /// + /// + /// Gets or sets the boundary parameter on the Content-Type header. + /// + /// The boundary. + /// + /// is null. + /// + public string Boundary { + get { return ContentType.Boundary; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (Boundary == value) + return; + + ContentType.Boundary = value.Trim (); + } + } + + internal byte[] RawPreamble { + get; set; + } + + /// + /// Get or set the preamble. + /// + /// + /// A multipart preamble appears before the first child entity of the + /// multipart and is typically used only in the top-level multipart + /// of the message to specify that the message is in MIME format and + /// therefore requires a MIME compliant email application to render + /// it correctly. + /// + /// The preamble. + public string Preamble { + get { + if (preamble == null && RawPreamble != null) + preamble = CharsetUtils.ConvertToUnicode (Headers.Options, RawPreamble, 0, RawPreamble.Length); + + return preamble; + } + set { + if (Preamble == value) + return; + + if (value != null) { + var folded = FoldPreambleOrEpilogue (FormatOptions.Default, value, false); + RawPreamble = Encoding.UTF8.GetBytes (folded); + preamble = folded; + } else { + RawPreamble = null; + preamble = null; + } + + WriteEndBoundary = true; + } + } + + internal byte[] RawEpilogue { + get; set; + } + + /// + /// Get or set the epilogue. + /// + /// + /// A multipart epiloque is the text that appears after the closing boundary + /// of the multipart and is typically either empty or a single new line + /// character sequence. + /// + /// The epilogue. + public string Epilogue { + get { + if (epilogue == null && RawEpilogue != null) { + int index = 0; + + // Note: In practice, the RawEpilogue contains the CRLF belonging to the end-boundary, but + // for sanity, we pretend that it doesn't. + if ((RawEpilogue.Length > 1 && RawEpilogue[0] == (byte) '\r' && RawEpilogue[1] == (byte) '\n')) + index += 2; + else if (RawEpilogue.Length > 1 && RawEpilogue[0] == (byte) '\n') + index++; + + epilogue = CharsetUtils.ConvertToUnicode (Headers.Options, RawEpilogue, index, RawEpilogue.Length - index); + } + + return epilogue; + } + set { + if (Epilogue == value) + return; + + if (value != null) { + var folded = FoldPreambleOrEpilogue (FormatOptions.Default, value, true); + RawEpilogue = Encoding.UTF8.GetBytes (folded); + epilogue = null; + } else { + RawEpilogue = null; + epilogue = null; + } + + WriteEndBoundary = true; + } + } + + /// + /// Get or set whether the end boundary should be written. + /// + /// + /// Gets or sets whether the end boundary should be written. + /// + /// true if the end boundary should be written; otherwise, false. + internal bool WriteEndBoundary { + get; set; + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipart (this); + } + + internal static string FoldPreambleOrEpilogue (FormatOptions options, string text, bool isEpilogue) + { + var builder = new StringBuilder (); + int startIndex, wordIndex; + int lineLength = 0; + int index = 0; + + if (isEpilogue) + builder.Append (options.NewLine); + + while (index < text.Length) { + startIndex = index; + + while (index < text.Length) { + if (!char.IsWhiteSpace (text[index])) + break; + + if (text[index] == '\n') { + builder.Append (options.NewLine); + startIndex = index + 1; + lineLength = 0; + } + + index++; + } + + wordIndex = index; + + while (index < text.Length && !char.IsWhiteSpace (text[index])) + index++; + + int length = index - startIndex; + + if (lineLength > 0 && lineLength + length >= options.MaxLineLength) { + builder.Append (options.NewLine); + length = index - wordIndex; + startIndex = wordIndex; + lineLength = 0; + } + + if (length > 0) { + builder.Append (text, startIndex, length); + lineLength += length; + } + } + + if (lineLength > 0) + builder.Append (options.NewLine); + + return builder.ToString (); + } + + static void WriteBytes (FormatOptions options, Stream stream, byte[] bytes, bool ensureNewLine, CancellationToken cancellationToken) + { + var cancellable = stream as ICancellableStream; + var filter = options.CreateNewLineFilter (ensureNewLine); + int index, length; + + var output = filter.Flush (bytes, 0, bytes.Length, out index, out length); + + if (cancellable != null) { + cancellable.Write (output, index, length, cancellationToken); + } else { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (output, index, length); + } + } + + static Task WriteBytesAsync (FormatOptions options, Stream stream, byte[] bytes, bool ensureNewLine, CancellationToken cancellationToken) + { + var filter = options.CreateNewLineFilter (ensureNewLine); + int index, length; + + var output = filter.Flush (bytes, 0, bytes.Length, out index, out length); + + return stream.WriteAsync (output, index, length, cancellationToken); + } + + /// + /// Prepare the MIME entity for transport using the specified encoding constraints. + /// + /// + /// Prepares the MIME entity for transport using the specified encoding constraints. + /// + /// The encoding constraint. + /// The maximum number of octets allowed per line (not counting the CRLF). Must be between 60 and 998 (inclusive). + /// + /// is not between 60 and 998 (inclusive). + /// -or- + /// is not a valid value. + /// + public override void Prepare (EncodingConstraint constraint, int maxLineLength = 78) + { + if (maxLineLength < FormatOptions.MinimumLineLength || maxLineLength > FormatOptions.MaximumLineLength) + throw new ArgumentOutOfRangeException (nameof (maxLineLength)); + + for (int i = 0; i < children.Count; i++) + children[i].Prepare (constraint, maxLineLength); + } + + /// + /// Write the to the specified output stream. + /// + /// + /// Writes the multipart MIME entity and its subparts to the output stream. + /// + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override void WriteTo (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + base.WriteTo (options, stream, contentOnly, cancellationToken); + + if (ContentType.IsMimeType ("multipart", "signed")) { + // don't reformat the headers or content of any children of a multipart/signed + if (options.International || options.HiddenHeaders.Count > 0) { + options = options.Clone (); + options.HiddenHeaders.Clear (); + options.International = false; + } + } + + var cancellable = stream as ICancellableStream; + + if (RawPreamble != null && RawPreamble.Length > 0) + WriteBytes (options, stream, RawPreamble, children.Count > 0 || EnsureNewLine, cancellationToken); + + var boundary = Encoding.ASCII.GetBytes ("--" + Boundary + "--"); + + if (cancellable != null) { + for (int i = 0; i < children.Count; i++) { + var msg = children[i] as MessagePart; + var multi = children[i] as Multipart; + var part = children[i] as MimePart; + + cancellable.Write (boundary, 0, boundary.Length - 2, cancellationToken); + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + children[i].WriteTo (options, stream, false, cancellationToken); + + if (msg != null && msg.Message != null && msg.Message.Body != null) { + multi = msg.Message.Body as Multipart; + part = msg.Message.Body as MimePart; + } + + if ((part != null && part.Content == null) || + (multi != null && !multi.WriteEndBoundary)) + continue; + + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } + + if (!WriteEndBoundary) + return; + + cancellable.Write (boundary, 0, boundary.Length, cancellationToken); + + if (RawEpilogue == null) + cancellable.Write (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken); + } else { + for (int i = 0; i < children.Count; i++) { + var rfc822 = children[i] as MessagePart; + var multi = children[i] as Multipart; + var part = children[i] as MimePart; + + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (boundary, 0, boundary.Length - 2); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + children[i].WriteTo (options, stream, false, cancellationToken); + + if (rfc822 != null && rfc822.Message != null && rfc822.Message.Body != null) { + multi = rfc822.Message.Body as Multipart; + part = rfc822.Message.Body as MimePart; + } + + if ((part != null && part.Content == null) || + (multi != null && !multi.WriteEndBoundary)) + continue; + + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + + if (!WriteEndBoundary) + return; + + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (boundary, 0, boundary.Length); + + if (RawEpilogue == null) { + cancellationToken.ThrowIfCancellationRequested (); + stream.Write (options.NewLineBytes, 0, options.NewLineBytes.Length); + } + } + + if (RawEpilogue != null && RawEpilogue.Length > 0) + WriteBytes (options, stream, RawEpilogue, EnsureNewLine, cancellationToken); + } + + /// + /// Asynchronously write the to the specified output stream. + /// + /// + /// Asynchronously writes the multipart MIME entity and its subparts to the output stream. + /// + /// An awaitable task. + /// The formatting options. + /// The output stream. + /// true if only the content should be written; otherwise, false. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + public override async Task WriteToAsync (FormatOptions options, Stream stream, bool contentOnly, CancellationToken cancellationToken = default (CancellationToken)) + { + await base.WriteToAsync (options, stream, contentOnly, cancellationToken).ConfigureAwait (false); + + if (ContentType.IsMimeType ("multipart", "signed")) { + // don't hide or reformat the headers of any children of a multipart/signed + if (options.International || options.HiddenHeaders.Count > 0) { + options = options.Clone (); + options.HiddenHeaders.Clear (); + options.International = false; + } + } + + if (RawPreamble != null && RawPreamble.Length > 0) + await WriteBytesAsync (options, stream, RawPreamble, children.Count > 0 || EnsureNewLine, cancellationToken).ConfigureAwait (false); + + var boundary = Encoding.ASCII.GetBytes ("--" + Boundary + "--"); + + for (int i = 0; i < children.Count; i++) { + var msg = children[i] as MessagePart; + var multi = children[i] as Multipart; + var part = children[i] as MimePart; + + await stream.WriteAsync (boundary, 0, boundary.Length - 2, cancellationToken).ConfigureAwait (false); + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + await children[i].WriteToAsync (options, stream, false, cancellationToken).ConfigureAwait (false); + + if (msg != null && msg.Message != null && msg.Message.Body != null) { + multi = msg.Message.Body as Multipart; + part = msg.Message.Body as MimePart; + } + + if ((part != null && part.Content == null) || + (multi != null && !multi.WriteEndBoundary)) + continue; + + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + } + + if (!WriteEndBoundary) + return; + + await stream.WriteAsync (boundary, 0, boundary.Length, cancellationToken).ConfigureAwait (false); + + if (RawEpilogue == null) + await stream.WriteAsync (options.NewLineBytes, 0, options.NewLineBytes.Length, cancellationToken).ConfigureAwait (false); + + if (RawEpilogue != null && RawEpilogue.Length > 0) + await WriteBytesAsync (options, stream, RawEpilogue, EnsureNewLine, cancellationToken).ConfigureAwait (false); + } + + #region ICollection implementation + + /// + /// Get the number of parts in the multipart. + /// + /// + /// Indicates the number of parts in the multipart. + /// + /// The number of parts in the multipart. + public int Count { + get { return children.Count; } + } + + /// + /// Get a value indicating whether this instance is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add an entity to the multipart. + /// + /// + /// Adds the specified part to the multipart. + /// + /// The part to add. + /// + /// is null. + /// + public void Add (MimeEntity part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + WriteEndBoundary = true; + children.Add (part); + } + + /// + /// Clear a multipart. + /// + /// + /// Removes all of the parts within the multipart. + /// + public void Clear () + { + WriteEndBoundary = true; + children.Clear (); + } + + /// + /// Check if the contains the specified part. + /// + /// + /// Determines whether or not the multipart contains the specified part. + /// + /// true if the specified part exists; + /// otherwise false. + /// The part to check for. + /// + /// is null. + /// + public bool Contains (MimeEntity part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return children.Contains (part); + } + + /// + /// Copy all of the entities in the to the specified array. + /// + /// + /// Copies all of the entities within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the headers to. + /// The index into the array. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void CopyTo (MimeEntity[] array, int arrayIndex) + { + children.CopyTo (array, arrayIndex); + } + + /// + /// Remove an entity from the multipart. + /// + /// + /// Removes the specified part, if it exists within the multipart. + /// + /// true if the part was removed; otherwise false. + /// The part to remove. + /// + /// is null. + /// + public bool Remove (MimeEntity part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + if (!children.Remove (part)) + return false; + + WriteEndBoundary = true; + + return true; + } + + #endregion + + #region IList implementation + + /// + /// Get the index of an entity. + /// + /// + /// Finds the index of the specified part, if it exists. + /// + /// The index of the specified part if found; otherwise -1. + /// The part. + /// + /// is null. + /// + public int IndexOf (MimeEntity part) + { + if (part == null) + throw new ArgumentNullException (nameof (part)); + + return children.IndexOf (part); + } + + /// + /// Insert an entity into the at the specified index. + /// + /// + /// Inserts the part into the multipart at the specified index. + /// + /// The index. + /// The part. + /// + /// is null. + /// + /// + /// is out of range. + /// + public void Insert (int index, MimeEntity part) + { + if (index < 0 || index > children.Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (part == null) + throw new ArgumentNullException (nameof (part)); + + children.Insert (index, part); + WriteEndBoundary = true; + } + + /// + /// Remove an entity from the at the specified index. + /// + /// + /// Removes the entity at the specified index. + /// + /// The index. + /// + /// is out of range. + /// + public void RemoveAt (int index) + { + children.RemoveAt (index); + WriteEndBoundary = true; + } + + /// + /// Get or set the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The entity at the specified index. + /// The index. + /// + /// is null. + /// + /// + /// is out of range. + /// + public MimeEntity this[int index] { + get { return children[index]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + WriteEndBoundary = true; + children[index] = value; + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get the enumerator for the children of the . + /// + /// + /// Gets the enumerator for the children of the . + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return children.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get the enumerator for the children of the . + /// + /// + /// Gets the enumerator for the children of the . + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return children.GetEnumerator (); + } + + #endregion + } +} diff --git a/src/MimeKit/MultipartAlternative.cs b/src/MimeKit/MultipartAlternative.cs new file mode 100644 index 0000000..1fd29ff --- /dev/null +++ b/src/MimeKit/MultipartAlternative.cs @@ -0,0 +1,184 @@ +// +// MultipartAlternative.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +using MimeKit.Text; + +namespace MimeKit { + /// + /// A multipart/alternative MIME entity. + /// + /// + /// A multipart/alternative MIME entity contains, as one might expect, is used to offer a list of + /// alternative formats for the main body of the message (usually they will be "text/plain" and + /// "text/html"). These alternatives are in order of increasing faithfulness to the original document + /// (in other words, the last entity will be in a format that, when rendered, will most closely match + /// what the sending client's WYSISYG editor produced). + /// + public class MultipartAlternative : Multipart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MultipartAlternative (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + /// An array of initialization parameters: headers and MIME entities. + /// + /// is null. + /// + /// + /// contains one or more arguments of an unknown type. + /// + public MultipartAlternative (params object[] args) : base ("alternative", args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + public MultipartAlternative () : base ("alternative") + { + } + + /// + /// Get the text of the text/plain alternative. + /// + /// + /// Gets the text of the text/plain alternative, if it exists. + /// + /// The text if a text/plain alternative exists; otherwise, null. + public string TextBody { + get { return GetTextBody (TextFormat.Plain); } + } + + /// + /// Get the HTML-formatted text of the text/html alternative. + /// + /// + /// Gets the HTML-formatted text of the text/html alternative, if it exists. + /// + /// The HTML if a text/html alternative exists; otherwise, null. + public string HtmlBody { + get { return GetTextBody (TextFormat.Html); } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipartAlternative (this); + } + + internal static string GetText (TextPart text) + { + if (text.IsFlowed) { + var converter = new FlowedToText (); + string delsp; + + if (text.ContentType.Parameters.TryGetValue ("delsp", out delsp)) + converter.DeleteSpace = delsp.ToLowerInvariant () == "yes"; + + return converter.Convert (text.Text); + } + + return text.Text; + } + + /// + /// Get the text body in the specified format. + /// + /// + /// Gets the text body in the specified format, if it exists. + /// + /// The text body in the desired format if it exists; otherwise, null. + /// The desired text format. + public string GetTextBody (TextFormat format) + { + // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful + for (int i = Count - 1; i >= 0; i--) { + var alternative = this[i] as MultipartAlternative; + + if (alternative != null) { + // Note: nested multipart/alternative parts make no sense... yet here we are. + return alternative.GetTextBody (format); + } + + var related = this[i] as MultipartRelated; + var text = this[i] as TextPart; + + if (related != null) { + var root = related.Root; + + alternative = root as MultipartAlternative; + if (alternative != null) + return alternative.GetTextBody (format); + + text = root as TextPart; + } + + if (text != null && text.IsFormat (format)) + return GetText (text); + } + + return null; + } + } +} diff --git a/src/MimeKit/MultipartRelated.cs b/src/MimeKit/MultipartRelated.cs new file mode 100644 index 0000000..7522c16 --- /dev/null +++ b/src/MimeKit/MultipartRelated.cs @@ -0,0 +1,344 @@ +// +// MultipartRelated.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; + +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A multipart/related MIME entity. + /// + /// + /// A multipart/related MIME entity contains, as one might expect, inter-related MIME parts which + /// typically reference each other via URIs based on the Content-Id and/or Content-Location headers. + /// + /// + /// + /// + public class MultipartRelated : Multipart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MultipartRelated (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + /// An array of initialization parameters: headers and MIME entities. + /// + /// is null. + /// + /// + /// contains one or more arguments of an unknown type. + /// + public MultipartRelated (params object[] args) : base ("related", args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + public MultipartRelated () : base ("related") + { + } + + int GetRootIndex () + { + var start = ContentType.Parameters["start"]; + + if (start != null) { + string contentId; + + if ((contentId = MimeUtils.EnumerateReferences (start).FirstOrDefault ()) == null) + contentId = start; + + var cid = new Uri (string.Format ("cid:{0}", contentId)); + + return IndexOf (cid); + } + + var type = ContentType.Parameters["type"]; + + if (type == null) + return -1; + + for (int index = 0; index < Count; index++) { + var mimeType = this[index].ContentType.MimeType; + + if (mimeType.Equals (type, StringComparison.OrdinalIgnoreCase)) + return index; + } + + return -1; + } + + /// + /// Gets or sets the root document of the multipart/related part and the appropriate Content-Type parameters. + /// + /// + /// Gets or sets the root document that references the other MIME parts within the multipart/related. + /// When getting the root document, the "start" parameter of the Content-Type header is used to + /// determine which of the parts is the root. If the "start" parameter does not exist or does not reference + /// any of the child parts, then the first child is assumed to be the root. + /// When setting the root document MIME part, the Content-Type header of the multipart/related part is also + /// updated with a appropriate "start" and "type" parameters. + /// + /// + /// + /// + /// The root MIME part. + /// + /// is null. + /// + public MimeEntity Root { + get { + int index = GetRootIndex (); + + if (index < 0 && Count == 0) + return null; + + return this[Math.Max (index, 0)]; + } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + int index; + + if (Count > 0) { + if ((index = GetRootIndex ()) != -1) { + this[index] = value; + } else { + Insert (0, value); + index = 0; + } + } else { + Add (value); + index = 0; + } + + if (string.IsNullOrEmpty (value.ContentId)) + value.ContentId = MimeUtils.GenerateMessageId (); + + ContentType.Parameters["type"] = value.ContentType.MediaType + "/" + value.ContentType.MediaSubtype; + + // Note: we only use a "start" parameter if the index of the root entity is not at index 0 in order + // to work around the following Thunderbird bug: https://bugzilla.mozilla.org/show_bug.cgi?id=471402 + if (index > 0) + ContentType.Parameters["start"] = "<" + value.ContentId + ">"; + else + ContentType.Parameters.Remove ("start"); + } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipartRelated (this); + } + + /// + /// Checks if the contains a part matching the specified URI. + /// + /// + /// Determines whether or not the multipart/related entity contains a part matching the specified URI. + /// + /// true if the specified part exists; otherwise false. + /// The URI of the MIME part. + /// + /// is null. + /// + public bool Contains (Uri uri) + { + return IndexOf (uri) != -1; + } + + /// + /// Gets the index of the part matching the specified URI. + /// + /// + /// Finds the index of the part matching the specified URI, if it exists. + /// If the URI scheme is "cid", then matching is performed based on the Content-Id header + /// values, otherwise the Content-Location headers are used. If the provided URI is absolute and a child + /// part's Content-Location is relative, then then the child part's Content-Location URI will be combined + /// with the value of its Content-Base header, if available, otherwise it will be combined with the + /// multipart/related part's Content-Base header in order to produce an absolute URI that can be + /// compared with the provided absolute URI. + /// + /// + /// + /// + /// The index of the part matching the specified URI if found; otherwise -1. + /// The URI of the MIME part. + /// + /// is null. + /// + public int IndexOf (Uri uri) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + bool cid = uri.IsAbsoluteUri && uri.Scheme.ToLowerInvariant () == "cid"; + + for (int index = 0; index < Count; index++) { + var entity = this[index]; + + if (uri.IsAbsoluteUri) { + if (cid) { + if (entity.ContentId == uri.AbsolutePath) + return index; + } else if (entity.ContentLocation != null) { + Uri absolute; + + if (!entity.ContentLocation.IsAbsoluteUri) { + if (entity.ContentBase != null) { + absolute = new Uri (entity.ContentBase, entity.ContentLocation); + } else if (ContentBase != null) { + absolute = new Uri (ContentBase, entity.ContentLocation); + } else { + continue; + } + } else { + absolute = entity.ContentLocation; + } + + if (absolute == uri) + return index; + } + } else if (entity.ContentLocation == uri) { + return index; + } + } + + return -1; + } + + /// + /// Opens a stream for reading the decoded content of the MIME part specified by the provided URI. + /// + /// + /// Opens a stream for reading the decoded content of the MIME part specified by the provided URI. + /// + /// A stream for reading the decoded content of the MIME part specified by the provided URI. + /// The URI. + /// The mime-type of the content. + /// The charset of the content (if the content is text-based) + /// + /// is null. + /// + /// + /// The MIME part for the specified URI could not be found. + /// + public Stream Open (Uri uri, out string mimeType, out string charset) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + int index = IndexOf (uri); + + if (index == -1) + throw new FileNotFoundException (); + + var part = this[index] as MimePart; + + if (part == null || part.Content == null) + throw new FileNotFoundException (); + + mimeType = part.ContentType.MimeType; + charset = part.ContentType.Charset; + + return part.Content.Open (); + } + + /// + /// Opens a stream for reading the decoded content of the MIME part specified by the provided URI. + /// + /// + /// Opens a stream for reading the decoded content of the MIME part specified by the provided URI. + /// + /// A stream for reading the decoded content of the MIME part specified by the provided URI. + /// The URI. + /// + /// is null. + /// + /// + /// The MIME part for the specified URI could not be found. + /// + public Stream Open (Uri uri) + { + if (uri == null) + throw new ArgumentNullException (nameof (uri)); + + int index = IndexOf (uri); + + if (index == -1) + throw new FileNotFoundException (); + + var part = this[index] as MimePart; + + if (part == null || part.Content == null) + throw new FileNotFoundException (); + + return part.Content.Open (); + } + } +} diff --git a/src/MimeKit/MultipartReport.cs b/src/MimeKit/MultipartReport.cs new file mode 100644 index 0000000..5754f09 --- /dev/null +++ b/src/MimeKit/MultipartReport.cs @@ -0,0 +1,151 @@ +// +// MultipartReport.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit { + /// + /// A multipart/report MIME entity. + /// + /// + /// A multipart/related MIME entity is a general container part for electronic mail + /// reports of any kind. + /// + /// + /// + /// + /// + /// + public class MultipartReport : Multipart + { + /// + /// Initialize a new instance of the class. + /// + /// + /// This constructor is used by . + /// + /// Information used by the constructor. + /// + /// is null. + /// + public MultipartReport (MimeEntityConstructorArgs args) : base (args) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + /// The type of the report. + /// An array of initialization parameters: headers and MIME entities. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains one or more arguments of an unknown type. + /// + public MultipartReport (string reportType, params object[] args) : base ("report", args) + { + if (reportType == null) + throw new ArgumentNullException (nameof (reportType)); + + ReportType = reportType; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new part. + /// + /// The type of the report. + /// + /// is null. + /// + public MultipartReport (string reportType) : base ("report") + { + if (reportType == null) + throw new ArgumentNullException (nameof (reportType)); + + ReportType = reportType; + } + + /// + /// Gets or sets the type of the report. + /// + /// + /// Gets or sets the type of the report. + /// The report type should be the subtype of the second + /// of the multipart/report. + /// + /// + /// + /// + /// The type of the report. + /// + /// is null. + /// + public string ReportType { + get { return ContentType.Parameters["report-type"]; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (ReportType == value) + return; + + ContentType.Parameters["report-type"] = value.Trim (); + } + } + + /// + /// Dispatches to the specific visit method for this MIME entity. + /// + /// + /// This default implementation for nodes + /// calls . Override this + /// method to call into a more specific method on a derived visitor class + /// of the class. However, it should still + /// support unknown visitors by calling + /// . + /// + /// The visitor. + /// + /// is null. + /// + public override void Accept (MimeVisitor visitor) + { + if (visitor == null) + throw new ArgumentNullException (nameof (visitor)); + + visitor.VisitMultipartReport (this); + } + } +} diff --git a/src/MimeKit/Parameter.cs b/src/MimeKit/Parameter.cs new file mode 100644 index 0000000..d773e86 --- /dev/null +++ b/src/MimeKit/Parameter.cs @@ -0,0 +1,713 @@ +// +// Parameter.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; + +using MimeKit.Encodings; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A header parameter as found in the Content-Type and Content-Disposition headers. + /// + /// + /// Content-Type and Content-Disposition headers often have parameters that specify + /// further information about how to interpret the content. + /// + public class Parameter + { + ParameterEncodingMethod encodingMethod; + Encoding encoding; + string text; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new parameter with the specified name and value. + /// + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// contains illegal characters. + /// + public Parameter (string name, string value) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("Parameter names are not allowed to be empty.", nameof (name)); + + for (int i = 0; i < name.Length; i++) { + if (name[i] > 127 || !IsAttr ((byte) name[i])) + throw new ArgumentException ("Illegal characters in parameter name.", nameof (name)); + } + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Value = value; + Name = name; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new parameter with the specified name and value. + /// + /// The character encoding. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// contains illegal characters. + /// + public Parameter (Encoding encoding, string name, string value) + { + if (encoding == null) + throw new ArgumentNullException (nameof (encoding)); + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("Parameter names are not allowed to be empty.", nameof (name)); + + for (int i = 0; i < name.Length; i++) { + if (name[i] > 127 || !IsAttr ((byte) name[i])) + throw new ArgumentException ("Illegal characters in parameter name.", nameof (name)); + } + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Encoding = encoding; + Value = value; + Name = name; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new parameter with the specified name and value. + /// + /// The character encoding. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// contains illegal characters. + /// + /// + /// is not supported. + /// + public Parameter (string charset, string name, string value) + { + if (charset == null) + throw new ArgumentNullException (nameof (charset)); + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("Parameter names are not allowed to be empty.", nameof (name)); + + for (int i = 0; i < name.Length; i++) { + if (name[i] > 127 || !IsAttr ((byte) name[i])) + throw new ArgumentException ("Illegal characters in parameter name.", nameof (name)); + } + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Encoding = CharsetUtils.GetEncoding (charset); + Value = value; + Name = name; + } + + /// + /// Gets the parameter name. + /// + /// + /// Gets the parameter name. + /// + /// The parameter name. + public string Name { + get; private set; + } + + /// + /// Gets or sets the parameter value character encoding. + /// + /// + /// Gets or sets the parameter value character encoding. + /// + /// The character encoding. + public Encoding Encoding { + get { return encoding ?? CharsetUtils.UTF8; } + set { + if (encoding == value) + return; + + encoding = value; + OnChanged (); + } + } + + /// + /// Gets or sets the parameter encoding method to use. + /// + /// + /// Gets or sets the parameter encoding method to use. + /// The MIME specifications specify that the proper method for encoding Content-Type + /// and Content-Disposition parameter values is the method described in + /// rfc2231. However, it is common for + /// some older email clients to improperly encode using the method described in + /// rfc2047 instead. + /// If set to , the encoding + /// method used will default to the value set on the . + /// + /// + /// + /// + /// The encoding method. + /// + /// is not a valid value. + /// + public ParameterEncodingMethod EncodingMethod { + get { return encodingMethod; } + set { + if (encodingMethod == value) + return; + + switch (value) { + case ParameterEncodingMethod.Default: + case ParameterEncodingMethod.Rfc2047: + case ParameterEncodingMethod.Rfc2231: + encodingMethod = value; + break; + default: + throw new ArgumentOutOfRangeException (nameof (value)); + } + + OnChanged (); + } + } + + /// + /// Gets or sets the parameter value. + /// + /// + /// Gets or sets the parameter value. + /// + /// The parameter value. + /// + /// is null. + /// + public string Value { + get { return text; } + set { + if (value == null) + throw new ArgumentNullException (nameof (value)); + + if (text == value) + return; + + text = value; + OnChanged (); + } + } + + static bool IsAttr (byte c) + { + return c.IsAttr (); + } + + static bool IsCtrl (char c) + { + return ((byte) c).IsCtrl (); + } + + enum EncodeMethod { + None, + Quote, + Rfc2047, + Rfc2231 + } + + EncodeMethod GetEncodeMethod (FormatOptions options, string name, string value, out string quoted) + { + var method = EncodeMethod.None; + EncodeMethod encode; + + switch (encodingMethod) { + default: + if (options.ParameterEncodingMethod == ParameterEncodingMethod.Rfc2231) + encode = EncodeMethod.Rfc2231; + else + encode = EncodeMethod.Rfc2047; + break; + case ParameterEncodingMethod.Rfc2231: + encode = EncodeMethod.Rfc2231; + break; + case ParameterEncodingMethod.Rfc2047: + encode = EncodeMethod.Rfc2047; + break; + } + + quoted = null; + + if (name.Length + 1 + value.Length >= options.MaxLineLength) + return encode; + + for (int i = 0; i < value.Length; i++) { + if (value[i] < 128) { + var c = (byte) value[i]; + + if (c.IsCtrl ()) + return encode; + + if (!c.IsAttr ()) + method = EncodeMethod.Quote; + } else if (options.International) { + method = EncodeMethod.Quote; + } else { + return encode; + } + } + + if (method == EncodeMethod.Quote) { + quoted = MimeUtils.Quote (value); + + if (name.Length + 1 + quoted.Length >= options.MaxLineLength) + return encode; + } + + return method; + } + + static EncodeMethod GetEncodeMethod (FormatOptions options, char[] value, int startIndex, int length) + { + var method = EncodeMethod.None; + + for (int i = startIndex; i < startIndex + length; i++) { + if (value[i] < 128) { + var c = (byte) value[i]; + + if (c.IsCtrl ()) + return EncodeMethod.Rfc2231; + + if (!c.IsAttr ()) + method = EncodeMethod.Quote; + } else if (options.International) { + method = EncodeMethod.Quote; + } else { + return EncodeMethod.Rfc2231; + } + } + + return method; + } + + static EncodeMethod GetEncodeMethod (byte[] value, int length) + { + var method = EncodeMethod.None; + + for (int i = 0; i < length; i++) { + if (value[i] >= 127 || value[i].IsCtrl ()) + return EncodeMethod.Rfc2231; + + if (!value[i].IsAttr ()) + method = EncodeMethod.Quote; + } + + return method; + } + + static Encoding GetBestEncoding (string value, Encoding defaultEncoding) + { + int encoding = 0; // us-ascii + + for (int i = 0; i < value.Length; i++) { + if (value[i] < 127) { + if (IsCtrl (value[i])) + encoding = Math.Max (encoding, 1); + } else if (value[i] < 256) { + encoding = Math.Max (encoding, 1); + } else { + encoding = 2; + } + } + + switch (encoding) { + case 0: return Encoding.ASCII; + case 1: return Encoding.GetEncoding (28591); // iso-8859-1 + default: return defaultEncoding; + } + } + + static bool Rfc2231GetNextValue (FormatOptions options, string charset, Encoder encoder, HexEncoder hex, char[] chars, ref int index, ref byte[] bytes, ref byte[] encoded, int maxLength, out string value) + { + int length = chars.Length - index; + + if (length < maxLength) { + switch (GetEncodeMethod (options, chars, index, length)) { + case EncodeMethod.Quote: + value = MimeUtils.Quote (new string (chars, index, length)); + index += length; + return false; + case EncodeMethod.None: + value = new string (chars, index, length); + index += length; + return false; + } + } + + length = Math.Min (maxLength, length); + int ratio, count, n; + + do { + count = encoder.GetByteCount (chars, index, length, true); + if (count > maxLength && length > 1) { + if ((ratio = (int) Math.Round ((double) count / (double) length)) > 1) + length -= Math.Max ((count - maxLength) / ratio, 1); + else + length--; + continue; + } + + if (bytes.Length < count) + Array.Resize (ref bytes, count); + + count = encoder.GetBytes (chars, index, length, bytes, 0, true); + + // Note: the first chunk needs to be encoded in order to declare the charset + if (index > 0 || charset == "us-ascii") { + var method = GetEncodeMethod (bytes, count); + + if (method == EncodeMethod.Quote) { + value = MimeUtils.Quote (Encoding.ASCII.GetString (bytes, 0, count)); + index += length; + return false; + } + + if (method == EncodeMethod.None) { + value = Encoding.ASCII.GetString (bytes, 0, count); + index += length; + return false; + } + } + + n = hex.EstimateOutputLength (count); + if (encoded.Length < n) + Array.Resize (ref encoded, n); + + // only the first value gets a charset declaration + int charsetLength = index == 0 ? charset.Length + 2 : 0; + + n = hex.Encode (bytes, 0, count, encoded); + if (n > 3 && (charsetLength + n) > maxLength) { + int x = 0; + + for (int i = n - 1; i >= 0 && charsetLength + i >= maxLength; i--) { + if (encoded[i] == (byte) '%') + x--; + else + x++; + } + + if ((ratio = (int) Math.Round ((double) count / (double) length)) > 1) + length -= Math.Max (x / ratio, 1); + else + length--; + continue; + } + + if (index == 0) + value = charset + "''" + Encoding.ASCII.GetString (encoded, 0, n); + else + value = Encoding.ASCII.GetString (encoded, 0, n); + index += length; + return true; + } while (true); + } + + void EncodeRfc2231 (FormatOptions options, StringBuilder builder, ref int lineLength, Encoding headerEncoding) + { + var bestEncoding = options.International ? CharsetUtils.UTF8 : GetBestEncoding (Value, encoding ?? headerEncoding); + int maxLength = options.MaxLineLength - (Name.Length + 6); + var charset = CharsetUtils.GetMimeCharset (bestEncoding); + var encoder = (Encoder) bestEncoding.GetEncoder (); + var bytes = new byte[Math.Max (maxLength, 6)]; + var hexbuf = new byte[bytes.Length * 3 + 3]; + var chars = Value.ToCharArray (); + var hex = new HexEncoder (); + int index = 0, i = 0; + string value, id; + bool encoded; + int length; + + do { + builder.Append (';'); + lineLength++; + + encoded = Rfc2231GetNextValue (options, charset, encoder, hex, chars, ref index, ref bytes, ref hexbuf, maxLength, out value); + length = Name.Length + (encoded ? 1 : 0) + 1 + value.Length; + + if (i == 0 && index == chars.Length) { + if (lineLength + 1 + length >= options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + builder.Append (Name); + if (encoded) + builder.Append ('*'); + builder.Append ('='); + builder.Append (value); + lineLength += length; + return; + } + + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + + id = i.ToString (); + length += id.Length + 1; + + builder.Append (Name); + builder.Append ('*'); + builder.Append (id); + if (encoded) + builder.Append ('*'); + builder.Append ('='); + builder.Append (value); + lineLength += length; + i++; + } while (index < chars.Length); + } + + static int EstimateEncodedWordLength (string charset, int byteCount, int encodeCount) + { + int length = charset.Length + 7; + + if ((double) encodeCount < (byteCount * 0.17)) { + // quoted-printable encoding + return length + (byteCount - encodeCount) + (encodeCount * 3); + } + + // base64 encoding + return length + ((byteCount + 2) / 3) * 4; + } + + static bool ExceedsMaxWordLength (string charset, int byteCount, int encodeCount, int maxLength) + { + int length = EstimateEncodedWordLength (charset, byteCount, encodeCount); + + return length + 1 >= maxLength; + } + + static int Rfc2047EncodeNextChunk (StringBuilder str, string text, ref int index, Encoding encoding, string charset, Encoder encoder, int maxLength) + { + int byteCount = 0, charCount = 0, encodeCount = 0; + var buffer = new char[2]; + int startIndex = index; + int nchars, n; + char c; + + while (index < text.Length) { + c = text[index++]; + + if (c < 127) { + if (IsCtrl (c) || c == '"' || c == '\\') + encodeCount++; + + byteCount++; + charCount++; + nchars = 1; + n = 1; + } else if (c < 256) { + // iso-8859-1 + encodeCount++; + byteCount++; + charCount++; + nchars = 1; + n = 1; + } else { + if (char.IsSurrogatePair (text, index - 1)) { + buffer[1] = text[index++]; + nchars = 2; + } else { + nchars = 1; + } + + buffer[0] = c; + + try { + n = encoder.GetByteCount (buffer, 0, nchars, true); + } catch { + n = 3; + } + + charCount += nchars; + encodeCount += n; + byteCount += n; + } + + if (ExceedsMaxWordLength (charset, byteCount, encodeCount, maxLength)) { + // restore our previous state + charCount -= nchars; + index -= nchars; + byteCount -= n; + break; + } + } + + return Rfc2047.AppendEncodedWord (str, encoding, text, startIndex, charCount, QEncodeMode.Text); + } + + void EncodeRfc2047 (FormatOptions options, StringBuilder builder, ref int lineLength, Encoding headerEncoding) + { + var bestEncoding = options.International ? CharsetUtils.UTF8 : GetBestEncoding (Value, encoding ?? headerEncoding); + var charset = CharsetUtils.GetMimeCharset (bestEncoding); + var encoder = (Encoder) bestEncoding.GetEncoder (); + int index = 0; + int length; + + builder.Append (';'); + lineLength++; + + // account for: + + "=\"=??b?<10 chars>?=\"" + if (lineLength + Name.Length + charset.Length + 10 + Math.Min (Value.Length, 10) >= options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + builder.AppendFormat ("{0}=\"", Name); + lineLength += Name.Length + 2; + + do { + length = Rfc2047EncodeNextChunk (builder, Value, ref index, bestEncoding, charset, encoder, (options.MaxLineLength - lineLength) - 1); + lineLength += length; + + if (index >= Value.Length) + break; + + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } while (true); + + builder.Append ('\"'); + lineLength++; + } + + internal void Encode (FormatOptions options, StringBuilder builder, ref int lineLength, Encoding headerEncoding) + { + string quoted; + + switch (GetEncodeMethod (options, Name, Value, out quoted)) { + case EncodeMethod.Rfc2231: + EncodeRfc2231 (options, builder, ref lineLength, headerEncoding); + break; + case EncodeMethod.Rfc2047: + EncodeRfc2047 (options, builder, ref lineLength, headerEncoding); + break; + case EncodeMethod.None: + quoted = Value; + goto default; + default: + builder.Append (';'); + lineLength++; + + if (lineLength + 1 + Name.Length + 1 + quoted.Length >= options.MaxLineLength) { + builder.Append (options.NewLine); + builder.Append ('\t'); + lineLength = 1; + } else { + builder.Append (' '); + lineLength++; + } + + lineLength += Name.Length + 1 + quoted.Length; + builder.Append (Name); + builder.Append ('='); + builder.Append (quoted); + break; + } + } + + /// + /// Returns a string representation of the . + /// + /// + /// Formats the parameter name and value in the form name="value". + /// + /// A string representation of the . + public override string ToString () + { + return Name + "=" + MimeUtils.Quote (Value); + } + + internal event EventHandler Changed; + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + } +} diff --git a/src/MimeKit/ParameterEncodingMethod.cs b/src/MimeKit/ParameterEncodingMethod.cs new file mode 100644 index 0000000..3670f6e --- /dev/null +++ b/src/MimeKit/ParameterEncodingMethod.cs @@ -0,0 +1,58 @@ +// +// ParameterEncodingMethod.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// The method to use for encoding Content-Type and Content-Disposition parameter values. + /// + /// + /// The MIME specifications specify that the proper method for encoding Content-Type and + /// Content-Disposition parameter values is the method described in + /// rfc2231. However, it is common for + /// some older email clients to improperly encode using the method described in + /// rfc2047 instead. + /// + /// + /// + /// + public enum ParameterEncodingMethod { + /// + /// Use the default encoding method set on the . + /// + Default = 0, + + /// + /// Use the encoding method described in rfc2231. + /// + Rfc2231 = (1 << 0), + + /// + /// Use the encoding method described in rfc2047 (for compatibility with older, + /// non-rfc-compliant email clients). + /// + Rfc2047 = (1 << 1) + } +} diff --git a/src/MimeKit/ParameterList.cs b/src/MimeKit/ParameterList.cs new file mode 100644 index 0000000..6f2bd91 --- /dev/null +++ b/src/MimeKit/ParameterList.cs @@ -0,0 +1,1092 @@ +// +// ParameterList.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Collections; +using System.Collections.Generic; + +using MimeKit.Encodings; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// A list of parameters, as found in the Content-Type and Content-Disposition headers. + /// + /// + /// Parameters are used by both and . + /// + public class ParameterList : IList + { + readonly Dictionary table; + readonly List parameters; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new parameter list. + /// + public ParameterList () + { + table = new Dictionary (MimeUtils.OrdinalIgnoreCase); + parameters = new List (); + } + + /// + /// Add a parameter with the specified name and value. + /// + /// + /// Adds a new parameter to the list with the specified name and value. + /// + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public void Add (string name, string value) + { + Add (new Parameter (name, value)); + } + + /// + /// Add a parameter with the specified name and value. + /// + /// + /// Adds a new parameter to the list with the specified name and value. + /// + /// The character encoding. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// contains illegal characters. + /// + public void Add (Encoding encoding, string name, string value) + { + Add (new Parameter (encoding, name, value)); + } + + /// + /// Add a parameter with the specified name and value. + /// + /// + /// Adds a new parameter to the list with the specified name and value. + /// + /// The character encoding. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be empty. + /// -or- + /// contains illegal characters. + /// + /// + /// is not supported. + /// + public void Add (string charset, string name, string value) + { + Add (new Parameter (charset, name, value)); + } + + /// + /// Check if the contains a parameter with the specified name. + /// + /// + /// Determines whether or not the parameter list contains a parameter with the specified name. + /// + /// true if the requested parameter exists; + /// otherwise false. + /// The parameter name. + /// + /// is null. + /// + public bool Contains (string name) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + return table.ContainsKey (name); + } + + /// + /// Get the index of the requested parameter, if it exists. + /// + /// + /// Finds the index of the parameter with the specified name, if it exists. + /// + /// The index of the requested parameter; otherwise -1. + /// The parameter name. + /// + /// is null. + /// + public int IndexOf (string name) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + for (int i = 0; i < parameters.Count; i++) { + if (name.Equals (parameters[i].Name, StringComparison.OrdinalIgnoreCase)) + return i; + } + + return -1; + } + + /// + /// Insert a parameter with the specified name and value at the given index. + /// + /// + /// Inserts a new parameter with the given name and value at the specified index. + /// + /// The index to insert the parameter. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + /// + /// is out of range. + /// + public void Insert (int index, string name, string value) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + Insert (index, new Parameter (name, value)); + } + + /// + /// Remove the specified parameter. + /// + /// + /// Removes the parameter with the specified name from the list, if it exists. + /// + /// true if the specified parameter was removed; + /// otherwise false. + /// The parameter name. + /// + /// is null. + /// + public bool Remove (string name) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Parameter param; + if (!table.TryGetValue (name, out param)) + return false; + + return Remove (param); + } + + /// + /// Get or set the value of a parameter with the specified name. + /// + /// + /// Gets or sets the value of a parameter with the specified name. + /// + /// The value of the specified parameter if it exists; otherwise null. + /// The parameter name. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The contains illegal characters. + /// + public string this [string name] { + get { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Parameter param; + if (table.TryGetValue (name, out param)) + return param.Value; + + return null; + } + set { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + Parameter param; + if (table.TryGetValue (name, out param)) { + param.Value = value; + } else { + Add (name, value); + } + } + } + + /// + /// Get the parameter with the specified name. + /// + /// + /// Gets the parameter with the specified name. + /// + /// + /// + /// + /// true if the parameter exists; otherwise, false. + /// The parameter name. + /// The parameter. + /// + /// is null. + /// + public bool TryGetValue (string name, out Parameter param) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + return table.TryGetValue (name, out param); + } + + /// + /// Get the value of the parameter with the specified name. + /// + /// + /// Gets the value of the parameter with the specified name. + /// + /// true if the parameter exists; otherwise, false. + /// The parameter name. + /// The parameter value. + /// + /// is null. + /// + public bool TryGetValue (string name, out string value) + { + Parameter param; + + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (!table.TryGetValue (name, out param)) { + value = null; + return false; + } + + value = param.Value; + + return true; + } + + #region ICollection implementation + + /// + /// Get the number of parameters in the . + /// + /// + /// Indicates the number of parameters in the list. + /// + /// The number of parameters. + public int Count { + get { return parameters.Count; } + } + + /// + /// Get a value indicating whether this instance is read only. + /// + /// + /// A is never read-only. + /// + /// true if this instance is read only; otherwise, false. + public bool IsReadOnly { + get { return false; } + } + + /// + /// Add a to a . + /// + /// + /// Adds the specified parameter to the end of the list. + /// + /// The parameter to add. + /// + /// The is null. + /// + /// + /// A parameter with the same name as + /// already exists. + /// + public void Add (Parameter param) + { + if (param == null) + throw new ArgumentNullException (nameof (param)); + + if (table.ContainsKey (param.Name)) + throw new ArgumentException ("A parameter of that name already exists.", nameof (param)); + + param.Changed += OnParamChanged; + table.Add (param.Name, param); + parameters.Add (param); + + OnChanged (); + } + + /// + /// Clear the parameter list. + /// + /// + /// Removes all of the parameters from the list. + /// + public void Clear () + { + foreach (var param in parameters) + param.Changed -= OnParamChanged; + + parameters.Clear (); + table.Clear (); + + OnChanged (); + } + + /// + /// Check if the contains the specified parameter. + /// + /// + /// Determines whether or not the parameter list contains the specified parameter. + /// + /// true if the specified parameter is contained; + /// otherwise false. + /// The parameter. + /// + /// The is null. + /// + public bool Contains (Parameter param) + { + if (param == null) + throw new ArgumentNullException (nameof (param)); + + return parameters.Contains (param); + } + + /// + /// Copy all of the parameters in the list to the specified array. + /// + /// + /// Copies all of the parameters within the into the array, + /// starting at the specified array index. + /// + /// The array to copy the parameters to. + /// The index into the array. + public void CopyTo (Parameter[] array, int arrayIndex) + { + parameters.CopyTo (array, arrayIndex); + } + + /// + /// Remove a from a . + /// + /// + /// Removes the specified parameter from the list. + /// + /// true if the specified parameter was removed; + /// otherwise false. + /// The parameter. + /// + /// The is null. + /// + public bool Remove (Parameter param) + { + if (param == null) + throw new ArgumentNullException (nameof (param)); + + if (!parameters.Remove (param)) + return false; + + param.Changed -= OnParamChanged; + table.Remove (param.Name); + + OnChanged (); + + return true; + } + + #endregion + + #region IList implementation + + /// + /// Ges the index of the requested parameter, if it exists. + /// + /// + /// Finds the index of the specified parameter, if it exists. + /// + /// The index of the requested parameter; otherwise -1. + /// The parameter. + /// + /// The is null. + /// + public int IndexOf (Parameter param) + { + if (param == null) + throw new ArgumentNullException (nameof (param)); + + return parameters.IndexOf (param); + } + + /// + /// Insert a at the specified index. + /// + /// + /// Inserts the parameter at the specified index in the list. + /// + /// The index to insert the parameter. + /// The parameter. + /// + /// The is null. + /// + /// + /// The is out of range. + /// + /// + /// A parameter with the same name as + /// already exists. + /// + public void Insert (int index, Parameter param) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (param == null) + throw new ArgumentNullException (nameof (param)); + + if (table.ContainsKey (param.Name)) + throw new ArgumentException ("A parameter of that name already exists.", nameof (param)); + + parameters.Insert (index, param); + table.Add (param.Name, param); + param.Changed += OnParamChanged; + + OnChanged (); + } + + /// + /// Remove the parameter at the specified index. + /// + /// + /// Removes the parameter at the specified index. + /// + /// The index. + /// + /// The is out of range. + /// + public void RemoveAt (int index) + { + if (index < 0 || index > Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + var param = parameters[index]; + + param.Changed -= OnParamChanged; + parameters.RemoveAt (index); + table.Remove (param.Name); + + OnChanged (); + } + + /// + /// Get or set the at the specified index. + /// + /// + /// Gets or sets the at the specified index. + /// + /// The parameter at the specified index. + /// The index. + /// + /// The is null. + /// + /// + /// The is out of range. + /// + /// + /// A parameter with the same name as + /// already exists. + /// + public Parameter this [int index] { + get { + return parameters[index]; + } + set { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof (index)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + var param = parameters[index]; + + if (param == value) + return; + + if (param.Name.Equals (value.Name, StringComparison.OrdinalIgnoreCase)) { + // replace the old param with the new one + if (table[param.Name] == param) + table[param.Name] = value; + } else if (table.ContainsKey (value.Name)) { + throw new ArgumentException ("A parameter of that name already exists.", nameof (value)); + } else { + table.Add (value.Name, value); + table.Remove (param.Name); + } + + param.Changed -= OnParamChanged; + value.Changed += OnParamChanged; + parameters[index] = value; + + OnChanged (); + } + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of parameters. + /// + /// + /// Gets an enumerator for the list of parameters. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return parameters.GetEnumerator (); + } + + #endregion + + #region IEnumerable implementation + + /// + /// Get an enumerator for the list of parameters. + /// + /// + /// Gets an enumerator for the list of parameters. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return parameters.GetEnumerator (); + } + + #endregion + + internal void Encode (FormatOptions options, StringBuilder builder, ref int lineLength, Encoding charset) + { + foreach (var param in parameters) + param.Encode (options, builder, ref lineLength, charset); + } + + /// + /// Serialize a to a string. + /// + /// + /// If there are multiple parameters in the list, they will be separated by a semicolon. + /// + /// A string representing the . + public override string ToString () + { + var values = new StringBuilder (); + + foreach (var param in parameters) { + values.Append ("; "); + values.Append (param.ToString ()); + } + + return values.ToString (); + } + + internal event EventHandler Changed; + + void OnParamChanged (object sender, EventArgs args) + { + OnChanged (); + } + + void OnChanged () + { + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + static bool SkipParamName (byte[] text, ref int index, int endIndex) + { + int startIndex = index; + + while (index < endIndex && text[index].IsAttr ()) + index++; + + return index > startIndex; + } + + class NameValuePair : IComparable + { + public int ValueLength; + public int ValueStart; + public bool Encoded; + public byte[] Value; + public string Name; + public int? Id; + + #region IComparable implementation + public int CompareTo (NameValuePair other) + { + if (!Id.HasValue) + return other.Id.HasValue ? -1 : 0; + + if (!other.Id.HasValue) + return 1; + + return Id.Value - other.Id.Value; + } + #endregion + } + + static bool TryParseNameValuePair (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out NameValuePair pair) + { + int valueIndex, valueLength, startIndex; + bool encoded = false; + int? id = null; + byte[] value; + string name; + + pair = null; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + startIndex = index; + if (!SkipParamName (text, ref index, endIndex)) { + if (throwOnError) + throw new ParseException (string.Format ("Invalid parameter name token at offset {0}", startIndex), startIndex, index); + + return false; + } + + name = Encoding.ASCII.GetString (text, startIndex, index - startIndex); + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (text[index] == (byte) '*') { + // the parameter is either encoded or it has a part id + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + + int identifier; + if (ParseUtils.TryParseInt32 (text, ref index, endIndex, out identifier)) { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + + if (text[index] == (byte) '*') { + encoded = true; + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + } + + id = identifier; + } else { + encoded = true; + } + } + + if (text[index] != (byte) '=') { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + + index++; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) { + if (throwOnError) + throw new ParseException (string.Format ("Incomplete parameter at offset {0}", startIndex), startIndex, index); + + return false; + } + + valueIndex = index; + value = text; + + if (text[index] == (byte) '"') { + ParseUtils.SkipQuoted (text, ref index, endIndex, throwOnError); + valueLength = index - valueIndex; + } else if (options.ParameterComplianceMode == RfcComplianceMode.Strict) { + ParseUtils.SkipToken (text, ref index, endIndex); + valueLength = index - valueIndex; + } else { + // Note: Google Docs, for example, does not always quote name/filename parameters + // with spaces in the name. See https://github.com/jstedfast/MimeKit/issues/106 + // for details. + while (index < endIndex && text[index] != (byte) ';' && text[index] != (byte) '\r' && text[index] != (byte) '\n') + index++; + + valueLength = index - valueIndex; + + if (index < endIndex && text[index] != (byte) ';') { + // Note: https://github.com/jstedfast/MimeKit/issues/159 adds to this suckage + // by having a multi-line unquoted value with spaces... don't you just love + // mail software written by people who have never heard of standards? + using (var memory = new MemoryStream ()) { + memory.Write (text, valueIndex, valueLength); + + do { + while (index < endIndex && (text[index] == (byte) '\r' || text[index] == (byte) '\n')) + index++; + + valueIndex = index; + + while (index < endIndex && text[index] != (byte) ';' && text[index] != (byte) '\r' && text[index] != (byte) '\n') + index++; + + memory.Write (text, valueIndex, index - valueIndex); + } while (index < endIndex && text[index] != ';'); + + value = memory.ToArray (); + valueLength = value.Length; + valueIndex = 0; + } + } + + // Trim trailing white space characters to work around issues such as the + // one described in https://github.com/jstedfast/MimeKit/issues/278 + while (valueLength > valueIndex && value[valueLength - 1].IsWhitespace ()) + valueLength--; + } + + pair = new NameValuePair { + ValueLength = valueLength, + ValueStart = valueIndex, + Encoded = encoded, + Value = value, + Name = name, + Id = id + }; + + return true; + } + + static bool TryGetCharset (byte[] text, ref int index, int endIndex, out string charset) + { + int startIndex = index; + int charsetEnd; + int i; + + charset = null; + + for (i = index; i < endIndex; i++) { + if (text[i] == (byte) '\'') + break; + } + + if (i == startIndex || i == endIndex) + return false; + + charsetEnd = i; + + for (i++; i < endIndex; i++) { + if (text[i] == (byte) '\'') + break; + } + + if (i == endIndex) + return false; + + charset = Encoding.ASCII.GetString (text, startIndex, charsetEnd - startIndex); + index = i + 1; + + return true; + } + + static string DecodeRfc2231 (out Encoding encoding, ref Decoder decoder, HexDecoder hex, byte[] text, int startIndex, int count, bool flush) + { + int endIndex = startIndex + count; + int index = startIndex; + string charset; + + // Note: decoder is only null if this is the first segment + if (decoder == null) { + if (TryGetCharset (text, ref index, endIndex, out charset)) { + try { + encoding = CharsetUtils.GetEncoding (charset, "?"); + decoder = (Decoder) encoding.GetDecoder (); + } catch (NotSupportedException) { + encoding = Encoding.GetEncoding (28591); // iso-8859-1 + decoder = (Decoder) encoding.GetDecoder (); + } + } else { + // When no charset is specified, it should be safe to assume US-ASCII... + // but we all know what assume means, right?? + encoding = Encoding.GetEncoding (28591); // iso-8859-1 + decoder = (Decoder) encoding.GetDecoder (); + } + } else { + encoding = null; + } + + int length = endIndex - index; + var decoded = new byte[hex.EstimateOutputLength (length)]; + + // hex decode... + length = hex.Decode (text, index, length, decoded); + + int outLength = decoder.GetCharCount (decoded, 0, length, flush); + var output = new char[outLength]; + + outLength = decoder.GetChars (decoded, 0, length, output, 0, flush); + + return new string (output, 0, outLength); + } + + internal static bool TryParse (ParserOptions options, byte[] text, ref int index, int endIndex, bool throwOnError, out ParameterList paramList) + { + var rfc2231 = new Dictionary> (MimeUtils.OrdinalIgnoreCase); + var @params = new List (); + List parts; + + paramList = null; + + do { + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (index >= endIndex) + break; + + // handle empty parameter name/value pairs + if (text[index] == (byte) ';') { + index++; + continue; + } + + NameValuePair pair; + if (!TryParseNameValuePair (options, text, ref index, endIndex, throwOnError, out pair)) + return false; + + if (!ParseUtils.SkipCommentsAndWhiteSpace (text, ref index, endIndex, throwOnError)) + return false; + + if (pair.Id.HasValue) { + if (rfc2231.TryGetValue (pair.Name, out parts)) { + parts.Add (pair); + } else { + parts = new List (); + rfc2231[pair.Name] = parts; + @params.Add (pair); + parts.Add (pair); + } + } else { + @params.Add (pair); + } + + if (index >= endIndex) + break; + + if (text[index] != (byte) ';') { + if (throwOnError) + throw new ParseException (string.Format ("Invalid parameter list token at offset {0}", index), index, index); + + return false; + } + + index++; + } while (true); + + paramList = new ParameterList (); + var hex = new HexDecoder (); + + foreach (var param in @params) { + var method = ParameterEncodingMethod.Default; + int startIndex = param.ValueStart; + int length = param.ValueLength; + var buffer = param.Value; + Encoding encoding = null; + Decoder decoder = null; + Parameter parameter; + string value; + + if (param.Id.HasValue) { + method = ParameterEncodingMethod.Rfc2231; + parts = rfc2231[param.Name]; + parts.Sort (); + + value = string.Empty; + + for (int i = 0; i < parts.Count; i++) { + startIndex = parts[i].ValueStart; + length = parts[i].ValueLength; + buffer = parts[i].Value; + + if (parts[i].Encoded) { + bool flush = i + 1 >= parts.Count || !parts[i + 1].Encoded; + Encoding charset; + + // Note: Some mail clients mistakenly quote encoded parameter values when they shouldn't + if (length >= 2 && buffer[startIndex] == (byte) '"' && buffer[startIndex + length - 1] == (byte) '"') { + startIndex++; + length -= 2; + } + + value += DecodeRfc2231 (out charset, ref decoder, hex, buffer, startIndex, length, flush); + encoding = encoding ?? charset; + } else if (length >= 2 && buffer[startIndex] == (byte) '"') { + var quoted = CharsetUtils.ConvertToUnicode (options,buffer, startIndex, length); + value += MimeUtils.Unquote (quoted); + hex.Reset (); + } else if (length > 0) { + value += CharsetUtils.ConvertToUnicode (options, buffer, startIndex, length); + hex.Reset (); + } + } + hex.Reset (); + } else if (param.Encoded) { + // Note: param value is not supposed to be quoted, but issue #239 illustrates + // that this can happen in the wild. Hopefully we will not need to worry + // about quoted-pairs. + if (length >= 2 && buffer[startIndex] == (byte) '"') { + if (buffer[startIndex + length - 1] == (byte) '"') + length--; + + startIndex++; + length--; + } + + value = DecodeRfc2231 (out encoding, ref decoder, hex, buffer, startIndex, length, true); + method = ParameterEncodingMethod.Rfc2231; + hex.Reset (); + } else if (!paramList.Contains (param.Name)) { + // Note: If we've got an rfc2231-encoded version of the same parameter, then + // we'll want to choose that one as opposed to the ASCII variant (i.e. this one). + // + // While most mail clients that I know of do not send multiple parameters of the + // same name, rfc6266 suggests that HTTP servers are using this approach to work + // around HTTP clients that do not (yet) implement support for the rfc2231 + // encoding of parameter values. Since none of the MIME specifications provide + // any suggestions for dealing with this, following rfc6266 seems to make the + // most sense, even though it is meant for HTTP clients and servers. + int codepage = -1; + + if (length >= 2 && text[startIndex] == (byte) '"') { + var quoted = Rfc2047.DecodeText (options, buffer, startIndex, length, out codepage); + value = MimeUtils.Unquote (quoted); + } else if (length > 0) { + value = Rfc2047.DecodeText (options, buffer, startIndex, length, out codepage); + } else { + value = string.Empty; + } + + if (codepage != -1 && codepage != 65001) { + encoding = CharsetUtils.GetEncoding (codepage); + method = ParameterEncodingMethod.Rfc2047; + } + } else { + continue; + } + + if (paramList.table.TryGetValue (param.Name, out parameter)) { + parameter.Encoding = encoding; + parameter.Value = value; + } else if (encoding != null) { + paramList.Add (encoding, param.Name, value); + parameter = paramList[paramList.Count - 1]; + } else { + paramList.Add (param.Name, value); + parameter = paramList[paramList.Count - 1]; + } + + parameter.EncodingMethod = method; + } + + return true; + } + } +} diff --git a/src/MimeKit/ParseException.cs b/src/MimeKit/ParseException.cs new file mode 100644 index 0000000..1e87e66 --- /dev/null +++ b/src/MimeKit/ParseException.cs @@ -0,0 +1,146 @@ +// +// ParseException.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +#if SERIALIZABLE +using System.Security; +using System.Runtime.Serialization; +#endif + +namespace MimeKit { + /// + /// A Parse exception as thrown by the various Parse methods in MimeKit. + /// + /// + /// A can be thrown by any of the Parse() methods + /// in MimeKit. Each exception instance will have a + /// which marks the byte offset of the token that failed to parse as well + /// as a which marks the byte offset where the error + /// occurred. + /// +#if SERIALIZABLE + [Serializable] +#endif + public class ParseException : FormatException + { +#if SERIALIZABLE + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The serialization info. + /// The stream context. + /// + /// is null. + /// + protected ParseException (SerializationInfo info, StreamingContext context) : base (info, context) + { + TokenIndex = info.GetInt32 ("TokenIndex"); + ErrorIndex = info.GetInt32 ("ErrorIndex"); + } +#endif + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The byte offset of the token. + /// The byte offset of the error. + /// The inner exception. + public ParseException (string message, int tokenIndex, int errorIndex, Exception innerException) : base (message, innerException) + { + TokenIndex = tokenIndex; + ErrorIndex = errorIndex; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The error message. + /// The byte offset of the token. + /// The byte offset of the error. + public ParseException (string message, int tokenIndex, int errorIndex) : base (message) + { + TokenIndex = tokenIndex; + ErrorIndex = errorIndex; + } + +#if SERIALIZABLE + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// Sets the + /// with information about the exception. + /// + /// The serialization info. + /// The streaming context. + /// + /// is null. + /// + [SecurityCritical] + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + + info.AddValue ("TokenIndex", TokenIndex); + info.AddValue ("ErrorIndex", ErrorIndex); + } +#endif + + /// + /// Gets the byte index of the token that was malformed. + /// + /// + /// The token index is the byte offset at which the token started. + /// + /// The byte index of the token. + public int TokenIndex { + get; private set; + } + + /// + /// Gets the index of the byte that caused the error. + /// + /// + /// The error index is the byte offset at which the parser encountered an error. + /// + /// The index of the byte that caused error. + public int ErrorIndex { + get; private set; + } + } +} diff --git a/src/MimeKit/ParserOptions.cs b/src/MimeKit/ParserOptions.cs new file mode 100644 index 0000000..e851f11 --- /dev/null +++ b/src/MimeKit/ParserOptions.cs @@ -0,0 +1,405 @@ +// +// ParserOptions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Text; +using System.Reflection; +using System.Collections.Generic; + +#if ENABLE_CRYPTO +using MimeKit.Cryptography; +#endif + +using MimeKit.Tnef; +using MimeKit.Utils; + +namespace MimeKit { + /// + /// Parser options as used by as well as various Parse and TryParse methods in MimeKit. + /// + /// + /// allows you to change and/or override default parsing options used by methods such + /// as and others. + /// + public class ParserOptions + { + readonly Dictionary mimeTypes = new Dictionary (StringComparer.Ordinal); + static readonly Type[] ConstructorArgTypes = { typeof (MimeEntityConstructorArgs) }; + + /// + /// The default parser options. + /// + /// + /// If a is not supplied to or other Parse and TryParse + /// methods throughout MimeKit, will be used. + /// + public static readonly ParserOptions Default = new ParserOptions (); + + /// + /// Gets or sets the compliance mode that should be used when parsing rfc822 addresses. + /// + /// + /// In general, you'll probably want this value to be + /// (the default) as it allows maximum interoperability with existing (broken) mail clients + /// and other mail software such as sloppily written perl scripts (aka spambots). + /// Even in mode, the address parser + /// is fairly liberal in what it accepts. Setting it to + /// just makes it try harder to deal with garbage input. + /// + /// The RFC compliance mode. + public RfcComplianceMode AddressParserComplianceMode { get; set; } + + /// + /// Gets or sets whether the rfc822 address parser should ignore unquoted commas in address names. + /// + /// + /// In general, you'll probably want this value to be true (the default) as it allows + /// maximum interoperability with existing (broken) mail clients and other mail software such as + /// sloppily written perl scripts (aka spambots) that do not properly quote the name when it + /// contains a comma. + /// + /// true if the address parser should ignore unquoted commas in address names; otherwise, false. + public bool AllowUnquotedCommasInAddresses { get; set; } + + /// + /// Gets or sets whether the rfc822 address parser should allow addresses without a domain. + /// + /// + /// In general, you'll probably want this value to be true (the default) as it allows + /// maximum interoperability with older email messages that may contain local UNIX addresses. + /// This option exists in order to allow parsing of mailbox addresses that do not have an + /// @domain component. These types of addresses are rare and were typically only used when sending + /// mail to other users on the same UNIX system. + /// + /// true if the address parser should allow mailbox addresses without a domain; otherwise, false. + public bool AllowAddressesWithoutDomain { get; set; } + + /// + /// Gets or sets the maximum address group depth the parser should accept. + /// + /// + /// This option exists in order to define the maximum recursive depth of an rfc822 group address + /// that the parser should accept before bailing out with the assumption that the address is maliciously + /// formed. If the value is set too large, then it is possible that a maliciously formed set of + /// recursive group addresses could cause a stack overflow. + /// + /// The maximum address group depth. + public int MaxAddressGroupDepth { get; set; } + + /// + /// Gets or sets the maximum MIME nesting depth the parser should accept. + /// + /// + /// This option exists in order to define the maximum recursive depth of MIME parts that the parser + /// should accept before treating further nesting as a leaf-node MIME part and not recursing any further. + /// If the value is set too large, then it is possible that a maliciously formed set of rdeeply nested + /// multipart MIME parts could cause a stack overflow. + /// + /// The maximum MIME nesting depth. + public int MaxMimeDepth { get; set; } + + /// + /// Gets or sets the compliance mode that should be used when parsing Content-Type and Content-Disposition parameters. + /// + /// + /// In general, you'll probably want this value to be + /// (the default) as it allows maximum interoperability with existing (broken) mail clients + /// and other mail software such as sloppily written perl scripts (aka spambots). + /// Even in mode, the parameter parser + /// is fairly liberal in what it accepts. Setting it to + /// just makes it try harder to deal with garbage input. + /// + /// The RFC compliance mode. + public RfcComplianceMode ParameterComplianceMode { get; set; } + + /// + /// Gets or sets the compliance mode that should be used when decoding rfc2047 encoded words. + /// + /// + /// In general, you'll probably want this value to be + /// (the default) as it allows maximum interoperability with existing (broken) mail clients + /// and other mail software such as sloppily written perl scripts (aka spambots). + /// + /// The RFC compliance mode. + public RfcComplianceMode Rfc2047ComplianceMode { get; set; } + + /// + /// Gets or sets a value indicating whether the Content-Length value should be + /// respected when parsing mbox streams. + /// + /// + /// For more details about why this may be useful, you can find more information + /// at + /// http://www.jwz.org/doc/content-length.html. + /// + /// true if the Content-Length value should be respected; + /// otherwise, false. + public bool RespectContentLength { get; set; } + + /// + /// Gets or sets the charset encoding to use as a fallback for 8bit headers. + /// + /// + /// and + /// + /// use this charset encoding as a fallback when decoding 8bit text into unicode. The first + /// charset encoding attempted is UTF-8, followed by this charset encoding, before finally + /// falling back to iso-8859-1. + /// + /// The charset encoding. + public Encoding CharsetEncoding { get; set; } + + /// + /// Initialize a new instance of the class. + /// + /// + /// By default, new instances of enable rfc2047 work-arounds + /// (which are needed for maximum interoperability with mail software used in the wild) + /// and do not respect the Content-Length header value. + /// + public ParserOptions () + { + AddressParserComplianceMode = RfcComplianceMode.Loose; + ParameterComplianceMode = RfcComplianceMode.Loose; + Rfc2047ComplianceMode = RfcComplianceMode.Loose; + CharsetEncoding = CharsetUtils.UTF8; + AllowUnquotedCommasInAddresses = true; + AllowAddressesWithoutDomain = true; + RespectContentLength = false; + MaxAddressGroupDepth = 3; + MaxMimeDepth = 1024; + } + + /// + /// Clones an instance of . + /// + /// + /// Clones a set of options, allowing you to change a specific option + /// without requiring you to change the original. + /// + /// An identical copy of the current instance. + public ParserOptions Clone () + { + var options = new ParserOptions (); + options.AddressParserComplianceMode = AddressParserComplianceMode; + options.AllowUnquotedCommasInAddresses = AllowUnquotedCommasInAddresses; + options.AllowAddressesWithoutDomain = AllowAddressesWithoutDomain; + options.ParameterComplianceMode = ParameterComplianceMode; + options.Rfc2047ComplianceMode = Rfc2047ComplianceMode; + options.MaxAddressGroupDepth = MaxAddressGroupDepth; + options.RespectContentLength = RespectContentLength; + options.CharsetEncoding = CharsetEncoding; + options.MaxMimeDepth = MaxMimeDepth; + + foreach (var mimeType in mimeTypes) + options.mimeTypes.Add (mimeType.Key, mimeType.Value); + + return options; + } + + /// + /// Registers the subclass for the specified mime-type. + /// + /// The MIME type. + /// A custom subclass of . + /// + /// Your custom class should not subclass + /// directly, but rather it should subclass + /// , , + /// , or one of their derivatives. + /// + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is not a subclass of , + /// , or . + /// -or- + /// does not have a constructor that takes + /// only a argument. + /// + public void RegisterMimeType (string mimeType, Type type) + { + if (mimeType == null) + throw new ArgumentNullException (nameof (mimeType)); + + if (type == null) + throw new ArgumentNullException (nameof (type)); + + mimeType = mimeType.ToLowerInvariant (); + +#if NETSTANDARD1_3 || NETSTANDARD1_6 + var info = type.GetTypeInfo (); +#else + var info = type; +#endif + + if (!info.IsSubclassOf (typeof (MessagePart)) && + !info.IsSubclassOf (typeof (Multipart)) && + !info.IsSubclassOf (typeof (MimePart))) + throw new ArgumentException ("The specified type must be a subclass of MessagePart, Multipart, or MimePart.", nameof (type)); + + var ctor = type.GetConstructor (ConstructorArgTypes); + + if (ctor == null) + throw new ArgumentException ("The specified type must have a constructor that takes a MimeEntityConstructorArgs argument.", nameof (type)); + + mimeTypes[mimeType] = ctor; + } + + static bool IsEncoded (IList
headers) + { + ContentEncoding encoding; + + for (int i = 0; i < headers.Count; i++) { + if (headers[i].Id != HeaderId.ContentTransferEncoding) + continue; + + MimeUtils.TryParse (headers[i].Value, out encoding); + + switch (encoding) { + case ContentEncoding.SevenBit: + case ContentEncoding.EightBit: + case ContentEncoding.Binary: + return false; + default: + return true; + } + } + + return false; + } + + internal MimeEntity CreateEntity (ContentType contentType, IList
headers, bool toplevel, int depth) + { + var args = new MimeEntityConstructorArgs (this, contentType, headers, toplevel); + + if (depth >= MaxMimeDepth) + return new MimePart (args); + + var subtype = contentType.MediaSubtype.ToLowerInvariant (); + var type = contentType.MediaType.ToLowerInvariant (); + + if (mimeTypes.Count > 0) { + var mimeType = string.Format ("{0}/{1}", type, subtype); + ConstructorInfo ctor; + + if (mimeTypes.TryGetValue (mimeType, out ctor)) + return (MimeEntity) ctor.Invoke (new object[] { args }); + } + + // Note: message/rfc822 and message/partial are not allowed to be encoded according to rfc2046 + // (sections 5.2.1 and 5.2.2, respectively). Since some broken clients will encode them anyway, + // it is necessary for us to treat those as opaque blobs instead, and thus the parser should + // parse them as normal MimeParts instead of MessageParts. + // + // Technically message/disposition-notification is only allowed to have use the 7bit encoding + // as well, but since MessageDispositionNotification is a MImePart subclass rather than a + // MessagePart subclass, it means that the content won't be parsed until later and so we can + // actually handle that w/o any problems. + if (type == "message") { + switch (subtype) { + case "global-disposition-notification": + case "disposition-notification": + return new MessageDispositionNotification (args); + case "global-delivery-status": + case "delivery-status": + return new MessageDeliveryStatus (args); + case "partial": + if (!IsEncoded (headers)) + return new MessagePartial (args); + break; + case "global-headers": + if (!IsEncoded (headers)) + return new TextRfc822Headers (args); + break; + case "external-body": + case "rfc2822": + case "rfc822": + case "global": + case "news": + if (!IsEncoded (headers)) + return new MessagePart (args); + break; + } + } + + if (type == "multipart") { + switch (subtype) { + case "alternative": + return new MultipartAlternative (args); + case "related": + return new MultipartRelated (args); + case "report": + return new MultipartReport (args); +#if ENABLE_CRYPTO + case "encrypted": + return new MultipartEncrypted (args); + case "signed": + return new MultipartSigned (args); +#endif + default: + return new Multipart (args); + } + } + + if (type == "application") { + switch (subtype) { +#if ENABLE_CRYPTO + case "x-pkcs7-signature": + case "pkcs7-signature": + return new ApplicationPkcs7Signature (args); + case "x-pgp-encrypted": + case "pgp-encrypted": + return new ApplicationPgpEncrypted (args); + case "x-pgp-signature": + case "pgp-signature": + return new ApplicationPgpSignature (args); + case "x-pkcs7-mime": + case "pkcs7-mime": + return new ApplicationPkcs7Mime (args); +#endif + case "vnd.ms-tnef": + case "ms-tnef": + return new TnefPart (args); + case "rtf": + return new TextPart (args); + } + } + + if (type == "text") { + if (subtype == "rfc822-headers" && !IsEncoded (headers)) + return new TextRfc822Headers (args); + + return new TextPart (args); + } + + return new MimePart (args); + } + } +} diff --git a/src/MimeKit/Properties/AssemblyInfo.cs b/src/MimeKit/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f559374 --- /dev/null +++ b/src/MimeKit/Properties/AssemblyInfo.cs @@ -0,0 +1,83 @@ +// +// AssemblyInfo.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle ("MimeKit")] +[assembly: AssemblyDescription ("A complete MIME library with support for S/MIME, PGP, DKIM and Unix mbox spools.")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany (".NET Foundation")] +[assembly: AssemblyProduct ("MimeKit")] +[assembly: AssemblyCopyright ("Copyright © 2013-2020 .NET Foundation and Contributors")] +[assembly: AssemblyTrademark (".NET Foundation")] +[assembly: AssemblyCulture ("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible (true)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid ("2fe79b66-d107-45da-9493-175f59c4a53c")] +[assembly: InternalsVisibleTo ("UnitTests, PublicKey=002400000480000094000000060200" + + "00002400005253413100040000110000003fefa5187022727c3471938d10df4c47d5d5ecbe2f36" + + "4656c5bfe4c47803453a91ae525f723f4316fd90a3f87366f4d948593277e950f6d2df6ee26068" + + "1877a6d9e71c3ea77e87e61f3878af1d69bf10dce8debe92c54ca8a10afc44dc08674f3db6594e" + + "f545d67d31cc3e18b8f90d8f220c4b67d7e87f5b7e8df410ac8faeb3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Micro Version +// Build Number +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +// +// Note: AssemblyVersion is what the CLR matches against at runtime, so be careful +// about updating it. The AssemblyFileVersion is the official release version while +// the AssemblyInformationalVersion is just used as a display version. +// +// Based on my current understanding, AssemblyVersion is essentially the "API Version" +// and so should only be updated when the API changes. The other 2 Version attributes +// represent the "Release Version". +// +// Making releases: +// +// If any classes, methods, or enum values have been added, bump the Micro Version +// in all version attributes and set the Build Number back to 0. +// +// If there have only been bug fixes, bump the Micro Version and/or the Build Number +// in the AssemblyFileVersion attribute. +[assembly: AssemblyInformationalVersion ("2.8.0.0")] +[assembly: AssemblyFileVersion ("2.8.0.0")] +[assembly: AssemblyVersion ("2.8.0.0")] diff --git a/src/MimeKit/RfcComplianceMode.cs b/src/MimeKit/RfcComplianceMode.cs new file mode 100644 index 0000000..2522f2f --- /dev/null +++ b/src/MimeKit/RfcComplianceMode.cs @@ -0,0 +1,45 @@ +// +// RfcComplianceMode.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit { + /// + /// An RFC compliance mode. + /// + /// + /// An RFC compliance mode. + /// + public enum RfcComplianceMode { + /// + /// Attempt to be much more liberal accepting broken and/or invalid formatting. + /// + Loose, + + /// + /// Do not attempt to be overly liberal in accepting broken and/or invalid formatting. + /// + Strict + } +} diff --git a/src/MimeKit/StreamExtensions.cs b/src/MimeKit/StreamExtensions.cs new file mode 100644 index 0000000..c8165fa --- /dev/null +++ b/src/MimeKit/StreamExtensions.cs @@ -0,0 +1,46 @@ +// +// StreamExtensions.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2015 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; + +namespace MimeKit { + static class StreamExtensions + { + public static void CopyTo (this Stream source, Stream destination, int bufferSize) + { + var buffer = new byte[bufferSize]; + int nread; + + while ((nread = source.Read (buffer, 0, bufferSize)) > 0) + destination.Write (buffer, 0, nread); + } + + public static void CopyTo (this Stream source, Stream destination) + { + CopyTo (source, destination, 4096); + } + } +} diff --git a/src/MimeKit/Text/CharBuffer.cs b/src/MimeKit/Text/CharBuffer.cs new file mode 100644 index 0000000..6afdac1 --- /dev/null +++ b/src/MimeKit/Text/CharBuffer.cs @@ -0,0 +1,93 @@ +// +// CharBuffer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Runtime.CompilerServices; + +namespace MimeKit.Text { + class CharBuffer + { + char[] buffer; + + public CharBuffer (int capacity) + { + buffer = new char[capacity]; + } + + public int Length { + [MethodImpl (MethodImplOptions.AggressiveInlining)] + get; + [MethodImpl (MethodImplOptions.AggressiveInlining)] + set; + } + + public char this[int index] { + [MethodImpl (MethodImplOptions.AggressiveInlining)] + get { return buffer[index]; } + [MethodImpl (MethodImplOptions.AggressiveInlining)] + set { buffer[index] = value; } + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + void EnsureCapacity (int length) + { + if (length < buffer.Length) + return; + + int capacity = buffer.Length << 1; + while (capacity <= length) + capacity <<= 1; + + Array.Resize (ref buffer, capacity); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public void Append (char c) + { + EnsureCapacity (Length + 1); + buffer[Length++] = c; + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public void Append (string str) + { + EnsureCapacity (Length + str.Length); + str.CopyTo (0, buffer, Length, str.Length); + Length += str.Length; + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public override string ToString () + { + return new string (buffer, 0, Length); + } + + public static implicit operator string (CharBuffer buffer) + { + return buffer.ToString (); + } + } +} diff --git a/src/MimeKit/Text/FlowedToHtml.cs b/src/MimeKit/Text/FlowedToHtml.cs new file mode 100644 index 0000000..22f77dc --- /dev/null +++ b/src/MimeKit/Text/FlowedToHtml.cs @@ -0,0 +1,423 @@ +// +// FlowedToHtml.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; + +namespace MimeKit.Text { + /// + /// A flowed text to HTML converter. + /// + /// + /// Used to convert flowed text (as described in rfc3676) into HTML. + /// + /// + /// + /// + public class FlowedToHtml : TextConverter + { + readonly UrlScanner scanner; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new flowed text to HTML converter. + /// + public FlowedToHtml () + { + scanner = new UrlScanner (); + + for (int i = 0; i < UrlPatterns.Count; i++) + scanner.Add (UrlPatterns[i]); + } + + /// + /// Get the input format. + /// + /// + /// Gets the input format. + /// + /// The input format. + public override TextFormat InputFormat { + get { return TextFormat.Flowed; } + } + + /// + /// Get the output format. + /// + /// + /// Gets the output format. + /// + /// The output format. + public override TextFormat OutputFormat { + get { return TextFormat.Html; } + } + + /// + /// Get or set whether the trailing space on a wrapped line should be deleted. + /// + /// + /// Gets or sets whether the trailing space on a wrapped line should be deleted. + /// The flowed text format defines a Content-Type parameter called "delsp" which can + /// have a value of "yes" or "no". If the parameter exists and the value is "yes", then + /// should be set to true, otherwise + /// should be set to false. + /// + /// + /// + /// + /// true if the trailing space on a wrapped line should be deleted; otherwise, false. + public bool DeleteSpace { + get; set; + } + + /// + /// Get or set the footer format. + /// + /// + /// Gets or sets the footer format. + /// + /// The footer format. + public HeaderFooterFormat FooterFormat { + get; set; + } + + /// + /// Get or set the header format. + /// + /// + /// Gets or sets the header format. + /// + /// The header format. + public HeaderFooterFormat HeaderFormat { + get; set; + } + + /// + /// Get or set the method to use for custom + /// filtering of HTML tags and content. + /// + /// + /// Get or set the method to use for custom + /// filtering of HTML tags and content. + /// + /// The html tag callback. + public HtmlTagCallback HtmlTagCallback { + get; set; + } + + /// + /// Get or set whether or not the converter should only output an HTML fragment. + /// + /// + /// Gets or sets whether or not the converter should only output an entire + /// HTML document or just a fragment of the HTML body content. + /// + /// true if the converter should only output an HTML fragment; otherwise, false. + public bool OutputHtmlFragment { + get; set; + } + + class FlowedToHtmlTagContext : HtmlTagContext + { + HtmlAttributeCollection attrs; + bool isEndTag; + + public FlowedToHtmlTagContext (HtmlTagId tag, HtmlAttribute attr) : base (tag) + { + attrs = new HtmlAttributeCollection (new [] { attr }); + } + + public FlowedToHtmlTagContext (HtmlTagId tag) : base (tag) + { + attrs = HtmlAttributeCollection.Empty; + } + + public override string TagName { + get { return TagId.ToHtmlTagName (); } + } + + public override HtmlAttributeCollection Attributes { + get { return attrs; } + } + + public override bool IsEmptyElementTag { + get { return TagId == HtmlTagId.Br; } + } + + public override bool IsEndTag { + get { return isEndTag; } + } + + public void SetIsEndTag (bool value) + { + isEndTag = value; + } + } + + static void DefaultHtmlTagCallback (HtmlTagContext tagContext, HtmlWriter htmlWriter) + { + tagContext.WriteTag (htmlWriter, true); + } + + static string Unquote (string line, out int quoteDepth) + { + int index = 0; + + quoteDepth = 0; + + if (line.Length == 0) + return line; + + while (index < line.Length && line[index] == '>') { + quoteDepth++; + index++; + } + + if (index > 0 && index < line.Length && line[index] == ' ') + index++; + + return index > 0 ? line.Substring (index) : line; + } + + static bool SuppressContent (IList stack) + { + for (int i = stack.Count; i > 0; i--) { + if (stack[i - 1].SuppressInnerContent) + return true; + } + + return false; + } + + void WriteText (HtmlWriter htmlWriter, string text) + { + var callback = HtmlTagCallback ?? DefaultHtmlTagCallback; + var content = text.ToCharArray (); + int endIndex = content.Length; + int startIndex = 0; + UrlMatch match; + int count; + + do { + count = endIndex - startIndex; + + if (scanner.Scan (content, startIndex, count, out match)) { + count = match.EndIndex - match.StartIndex; + + if (match.StartIndex > startIndex) { + // write everything up to the match + htmlWriter.WriteText (content, startIndex, match.StartIndex - startIndex); + } + + var href = match.Prefix + new string (content, match.StartIndex, count); + var ctx = new FlowedToHtmlTagContext (HtmlTagId.A, new HtmlAttribute (HtmlAttributeId.Href, href)); + callback (ctx, htmlWriter); + + if (!ctx.SuppressInnerContent) + htmlWriter.WriteText (content, match.StartIndex, count); + + if (!ctx.DeleteEndTag) { + ctx.SetIsEndTag (true); + + if (ctx.InvokeCallbackForEndTag) + callback (ctx, htmlWriter); + else + ctx.WriteTag (htmlWriter); + } + + startIndex = match.EndIndex; + } else { + htmlWriter.WriteText (content, startIndex, count); + break; + } + } while (startIndex < endIndex); + } + + void WriteParagraph (HtmlWriter htmlWriter, IList stack, ref int currentQuoteDepth, StringBuilder para, int quoteDepth) + { + var callback = HtmlTagCallback ?? DefaultHtmlTagCallback; + FlowedToHtmlTagContext ctx; + + while (currentQuoteDepth < quoteDepth) { + ctx = new FlowedToHtmlTagContext (HtmlTagId.BlockQuote); + callback (ctx, htmlWriter); + currentQuoteDepth++; + stack.Add (ctx); + } + + while (quoteDepth < currentQuoteDepth) { + ctx = stack[stack.Count - 1]; + stack.RemoveAt (stack.Count - 1); + + if (!SuppressContent (stack) && !ctx.DeleteEndTag) { + ctx.SetIsEndTag (true); + + if (ctx.InvokeCallbackForEndTag) + callback (ctx, htmlWriter); + else + ctx.WriteTag (htmlWriter); + } + + if (ctx.TagId == HtmlTagId.BlockQuote) + currentQuoteDepth--; + } + + if (SuppressContent (stack)) + return; + + ctx = new FlowedToHtmlTagContext (para.Length == 0 ? HtmlTagId.Br : HtmlTagId.P); + callback (ctx, htmlWriter); + + if (para.Length > 0) { + if (!ctx.SuppressInnerContent) + WriteText (htmlWriter, para.ToString ()); + + if (!ctx.DeleteEndTag) { + ctx.SetIsEndTag (true); + + if (ctx.InvokeCallbackForEndTag) + callback (ctx, htmlWriter); + else + ctx.WriteTag (htmlWriter); + } + } + + if (!ctx.DeleteTag) + htmlWriter.WriteMarkupText (Environment.NewLine); + } + + /// + /// Convert the contents of from the to the + /// and uses the to write the resulting text. + /// + /// + /// Converts the contents of from the to the + /// and uses the to write the resulting text. + /// + /// The text reader. + /// The text writer. + /// + /// is null. + /// -or- + /// is null. + /// + public override void Convert (TextReader reader, TextWriter writer) + { + if (reader == null) + throw new ArgumentNullException (nameof (reader)); + + if (writer == null) + throw new ArgumentNullException (nameof (writer)); + + if (!OutputHtmlFragment) + writer.Write (""); + + if (!string.IsNullOrEmpty (Header)) { + if (HeaderFormat == HeaderFooterFormat.Text) { + var converter = new TextToHtml { OutputHtmlFragment = true }; + + using (var sr = new StringReader (Header)) + converter.Convert (sr, writer); + } else { + writer.Write (Header); + } + } + + using (var htmlWriter = new HtmlWriter (writer)) { + var callback = HtmlTagCallback ?? DefaultHtmlTagCallback; + var stack = new List (); + var para = new StringBuilder (); + int currentQuoteDepth = 0; + int paraQuoteDepth = -1; + int quoteDepth; + string line; + + while ((line = reader.ReadLine ()) != null) { + // unquote the line + line = Unquote (line, out quoteDepth); + + // remove space-stuffing + if (line.Length > 0 && line[0] == ' ') + line = line.Substring (1); + + if (para.Length == 0) { + paraQuoteDepth = quoteDepth; + } else if (quoteDepth != paraQuoteDepth) { + // Note: according to rfc3676, when a folded line has a different quote + // depth than the previous line, then quote-depth rules win and we need + // to treat this as a new paragraph. + WriteParagraph (htmlWriter, stack, ref currentQuoteDepth, para, paraQuoteDepth); + paraQuoteDepth = quoteDepth; + para.Length = 0; + } + + para.Append (line); + + if (line.Length == 0 || line[line.Length - 1] != ' ') { + // line did not end with a space, so the next line will start a new paragraph + WriteParagraph (htmlWriter, stack, ref currentQuoteDepth, para, paraQuoteDepth); + paraQuoteDepth = 0; + para.Length = 0; + } else if (DeleteSpace) { + // Note: lines ending with a space mean that the next line is a continuation + para.Length--; + } + } + + for (int i = stack.Count; i > 0; i--) { + var ctx = stack[i - 1]; + + ctx.SetIsEndTag (true); + + if (ctx.InvokeCallbackForEndTag) + callback (ctx, htmlWriter); + else + ctx.WriteTag (htmlWriter); + } + + htmlWriter.Flush (); + } + + if (!string.IsNullOrEmpty (Footer)) { + if (FooterFormat == HeaderFooterFormat.Text) { + var converter = new TextToHtml { OutputHtmlFragment = true }; + + using (var sr = new StringReader (Footer)) + converter.Convert (sr, writer); + } else { + writer.Write (Footer); + } + } + + if (!OutputHtmlFragment) + writer.Write (""); + } + } +} diff --git a/src/MimeKit/Text/FlowedToText.cs b/src/MimeKit/Text/FlowedToText.cs new file mode 100644 index 0000000..3b0092f --- /dev/null +++ b/src/MimeKit/Text/FlowedToText.cs @@ -0,0 +1,177 @@ +// +// FlowedToText.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Text; + +namespace MimeKit.Text { + /// + /// A flowed text to text converter. + /// + /// + /// Unwraps the flowed text format described in rfc3676. + /// + public class FlowedToText : TextConverter + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new flowed text to text converter. + /// + public FlowedToText () + { + } + + /// + /// Get the input format. + /// + /// + /// Gets the input format. + /// + /// The input format. + public override TextFormat InputFormat { + get { return TextFormat.Flowed; } + } + + /// + /// Get the output format. + /// + /// + /// Gets the output format. + /// + /// The output format. + public override TextFormat OutputFormat { + get { return TextFormat.Plain; } + } + + /// + /// Get or set whether the trailing space on a wrapped line should be deleted. + /// + /// + /// Gets or sets whether the trailing space on a wrapped line should be deleted. + /// The flowed text format defines a Content-Type parameter called "delsp" which can + /// have a value of "yes" or "no". If the parameter exists and the value is "yes", then + /// should be set to true, otherwise + /// should be set to false. + /// + /// true if the trailing space on a wrapped line should be deleted; otherwise, false. + public bool DeleteSpace { + get; set; + } + + static string Unquote (string line, out int quoteDepth) + { + int index = 0; + + quoteDepth = 0; + + if (line.Length == 0) + return line; + + while (index < line.Length && line[index] == '>') { + quoteDepth++; + index++; + } + + if (index > 0 && index < line.Length && line[index] == ' ') + index++; + + return index > 0 ? line.Substring (index) : line; + } + + /// + /// Convert the contents of from the to the + /// and uses the to write the resulting text. + /// + /// + /// Converts the contents of from the to the + /// and uses the to write the resulting text. + /// + /// The text reader. + /// The text writer. + /// + /// is null. + /// -or- + /// is null. + /// + public override void Convert (TextReader reader, TextWriter writer) + { + StringBuilder para = new StringBuilder (); + int paraQuoteDepth = -1; + int quoteDepth; + string line; + + if (reader == null) + throw new ArgumentNullException (nameof (reader)); + + if (writer == null) + throw new ArgumentNullException (nameof (writer)); + + if (!string.IsNullOrEmpty (Header)) + writer.Write (Header); + + while ((line = reader.ReadLine ()) != null) { + line = Unquote (line, out quoteDepth); + + // if there is a leading space, it was stuffed + if (quoteDepth == 0 && line.Length > 0 && line[0] == ' ') + line = line.Substring (1); + + if (paraQuoteDepth == -1) { + paraQuoteDepth = quoteDepth; + } else if (quoteDepth != paraQuoteDepth) { + // Note: according to rfc3676, when a folded line has a different quote + // depth than the previous line, then quote-depth rules win and we need + // to treat this as a new paragraph. + if (paraQuoteDepth > 0) + writer.Write (new string ('>', paraQuoteDepth) + " "); + writer.WriteLine (para); + paraQuoteDepth = quoteDepth; + para.Length = 0; + } + + para.Append (line); + + if (line.Length == 0 || line[line.Length - 1] != ' ') { + // when a line does not end with a space, then the paragraph has ended + if (paraQuoteDepth > 0) + writer.Write (new string ('>', paraQuoteDepth) + " "); + writer.WriteLine (para); + paraQuoteDepth = -1; + para.Length = 0; + } else if (DeleteSpace) { + // Note: lines ending with a space mean that the next line is a continuation + para.Length--; + } + } + + if (!string.IsNullOrEmpty (Footer)) + writer.Write (Footer); + } + } +} diff --git a/src/MimeKit/Text/HeaderFooterFormat.cs b/src/MimeKit/Text/HeaderFooterFormat.cs new file mode 100644 index 0000000..a5bd0eb --- /dev/null +++ b/src/MimeKit/Text/HeaderFooterFormat.cs @@ -0,0 +1,45 @@ +// +// HeaderFooterFormat.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Text { + /// + /// An enumeration of possible header and footer formats. + /// + /// + /// An enumeration of possible header and footer formats. + /// + public enum HeaderFooterFormat { + /// + /// The header or footer contains plain text. + /// + Text, + + /// + /// The header or footer contains properly formatted HTML. + /// + Html + } +} diff --git a/src/MimeKit/Text/HtmlAttribute.cs b/src/MimeKit/Text/HtmlAttribute.cs new file mode 100644 index 0000000..3586c59 --- /dev/null +++ b/src/MimeKit/Text/HtmlAttribute.cs @@ -0,0 +1,143 @@ +// +// HtmlAttribute.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Text { + /// + /// An HTML attribute. + /// + /// + /// An HTML attribute. + /// + /// + /// + /// + public class HtmlAttribute + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new HTML attribute with the given id and value. + /// + /// The attribute identifier. + /// The attribute value. + /// + /// is not a valid value. + /// + public HtmlAttribute (HtmlAttributeId id, string value) + { + if (id == HtmlAttributeId.Unknown) + throw new ArgumentOutOfRangeException (nameof (id)); + + Name = id.ToAttributeName (); + Value = value; + Id = id; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new HTML attribute with the given name and value. + /// + /// The attribute name. + /// The attribute value. + /// + /// is null. + /// + public HtmlAttribute (string name, string value) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("The attribute name cannot be empty.", nameof (name)); + + if (!HtmlUtils.IsValidTokenName (name)) + throw new ArgumentException ("Invalid attribute name.", nameof (name)); + + Id = name.ToHtmlAttributeId (); + Value = value; + Name = name; + } + + internal HtmlAttribute (string name) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (name.Length == 0) + throw new ArgumentException ("The attribute name cannot be empty.", nameof (name)); + + Id = name.ToHtmlAttributeId (); + Name = name; + } + + /// + /// Get the HTML attribute identifier. + /// + /// + /// Gets the HTML attribute identifier. + /// + /// + /// + /// + /// The attribute identifier. + public HtmlAttributeId Id { + get; private set; + } + + /// + /// Get the name of the attribute. + /// + /// + /// Gets the name of the attribute. + /// + /// + /// + /// + /// The name of the attribute. + public string Name { + get; private set; + } + + /// + /// Get the value of the attribute. + /// + /// + /// Gets the value of the attribute. + /// + /// + /// + /// + /// The value of the attribute. + public string Value { + get; internal set; + } + } +} diff --git a/src/MimeKit/Text/HtmlAttributeCollection.cs b/src/MimeKit/Text/HtmlAttributeCollection.cs new file mode 100644 index 0000000..e805c2f --- /dev/null +++ b/src/MimeKit/Text/HtmlAttributeCollection.cs @@ -0,0 +1,125 @@ +// +// HtmlAttributeCollection.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MimeKit.Text { + /// + /// A readonly collection of HTML attributes. + /// + /// + /// A readonly collection of HTML attributes. + /// + public class HtmlAttributeCollection : IEnumerable + { + /// + /// An empty attribute collection. + /// + /// + /// An empty attribute collection. + /// + public static readonly HtmlAttributeCollection Empty = new HtmlAttributeCollection (); + + readonly List attributes = new List (); + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// A collection of attributes. + public HtmlAttributeCollection (IEnumerable collection) + { + attributes = new List (collection); + } + + internal HtmlAttributeCollection () + { + attributes = new List (); + } + + /// + /// Get the number of attributes in the collection. + /// + /// + /// Gets the number of attributes in the collection. + /// + /// The number of attributes in the collection. + public int Count { + get { return attributes.Count; } + } + + internal void Add (HtmlAttribute attribute) + { + if (attribute == null) + throw new ArgumentNullException (nameof (attribute)); + + attributes.Add (attribute); + } + + /// + /// Get the at the specified index. + /// + /// + /// Gets the at the specified index. + /// + /// The HTML attribute at the specified index. + /// The index. + /// + /// is out of range. + /// + public HtmlAttribute this[int index] { + get { return attributes[index]; } + } + + /// + /// Gets an enumerator for the attribute collection. + /// + /// + /// Gets an enumerator for the attribute collection. + /// + /// The enumerator. + public IEnumerator GetEnumerator () + { + return attributes.GetEnumerator (); + } + + /// + /// Gets an enumerator for the attribute collection. + /// + /// + /// Gets an enumerator for the attribute collection. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator () + { + return attributes.GetEnumerator (); + } + } +} diff --git a/src/MimeKit/Text/HtmlAttributeId.cs b/src/MimeKit/Text/HtmlAttributeId.cs new file mode 100644 index 0000000..7c61ead --- /dev/null +++ b/src/MimeKit/Text/HtmlAttributeId.cs @@ -0,0 +1,656 @@ +// +// HtmlAttributeId.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit.Text { + /// + /// HTML attribute identifiers. + /// + /// + /// HTML attribute identifiers. + /// + public enum HtmlAttributeId { + /// + /// An unknown HTML attribute identifier. + /// + Unknown, + + /// + /// The "abbr" attribute. + /// + Abbr, + + /// + /// The "accept" attribute. + /// + Accept, + + /// + /// The "accept-charset" attribute. + /// + [HtmlAttributeName ("accept-charset")] + AcceptCharset, + + /// + /// The "accesskey" attribute. + /// + AccessKey, + + /// + /// The "action" attribute. + /// + Action, + + /// + /// The "align" attribute. + /// + Align, + + /// + /// The "alink" attribute. + /// + Alink, + + /// + /// The "alt" attribute. + /// + Alt, + + /// + /// The "archive" attribute. + /// + Archive, + + /// + /// The "axis" attribute. + /// + Axis, + + /// + /// The "background" attribute. + /// + Background, + + /// + /// The "bgcolor" attribute. + /// + BGColor, + + /// + /// The "border" attribute. + /// + Border, + + /// + /// The "cellpadding" attribute. + /// + CellPadding, + + /// + /// The "cellspacing" attribute. + /// + CellSpacing, + + /// + /// The "char" attribute. + /// + Char, + + /// + /// The "charoff" attribute. + /// + CharOff, + + /// + /// The "charset" attribute. + /// + Charset, + + /// + /// The "checked" attribute. + /// + Checked, + + /// + /// The "cite" attribute. + /// + Cite, + + /// + /// The "class" attribute. + /// + Class, + + /// + /// The "classid" attribute. + /// + ClassId, + + /// + /// The "clear" attribute. + /// + Clear, + + /// + /// The "code" attribute. + /// + Code, + + /// + /// The "codebase" attribute. + /// + CodeBase, + + /// + /// The "codetype" attribute. + /// + CodeType, + + /// + /// The "color" attribute. + /// + Color, + + /// + /// The "cols" attribute. + /// + Cols, + + /// + /// The "colspan" attribute. + /// + ColSpan, + + /// + /// The "compact" attribute. + /// + Compact, + + /// + /// The "content" attribute. + /// + Content, + + /// + /// The "coords" attribute. + /// + Coords, + + /// + /// The "data" attribute. + /// + Data, + + /// + /// The "datetime" attribute. + /// + DateTime, + + /// + /// The "declare" attribute. + /// + Declare, + + /// + /// The "defer" attribute. + /// + Defer, + + /// + /// The "dir" attribute. + /// + Dir, + + /// + /// The "disabled" attribute. + /// + Disabled, + + /// + /// The "dynsrc" attribute. + /// + DynSrc, + + /// + /// The "enctype" attribute. + /// + EncType, + + /// + /// The "face" attribute. + /// + Face, + + /// + /// The "for" attribute. + /// + For, + + /// + /// The "frame" attribute. + /// + Frame, + + /// + /// The "frameborder" attribute. + /// + FrameBorder, + + /// + /// The "headers" attribute. + /// + Headers, + + /// + /// The "height" attribute. + /// + Height, + + /// + /// The "href" attribute. + /// + Href, + + /// + /// The "hreflang" attribute. + /// + HrefLang, + + /// + /// The "hspace" attribute. + /// + Hspace, + + /// + /// The "http-equiv" attribute. + /// + [HtmlAttributeName ("http-equiv")] + HttpEquiv, + + /// + /// The "id" attribute. + /// + Id, + + /// + /// The "ismap" attribute. + /// + IsMap, + + /// + /// The "label" attribute. + /// + Label, + + /// + /// The "lang" attribute. + /// + Lang, + + /// + /// The "language" attribute. + /// + Language, + + /// + /// The "leftmargin" attribute. + /// + LeftMargin, + + /// + /// The "link" attribute. + /// + Link, + + /// + /// The "longdesc" attribute. + /// + LongDesc, + + /// + /// The "lowsrc" attribute. + /// + LowSrc, + + /// + /// The "marginheight" attribute. + /// + MarginHeight, + + /// + /// The "marginwidth" attribute. + /// + MarginWidth, + + /// + /// The "maxlength" attribute. + /// + MaxLength, + + /// + /// The "media" attribute. + /// + Media, + + /// + /// The "method" attribute. + /// + Method, + + /// + /// The "multiple" attribute. + /// + Multiple, + + /// + /// The "name" attribute. + /// + Name, + + /// + /// The "nohref" attribute. + /// + NoHref, + + /// + /// The "noresize" attribute. + /// + NoResize, + + /// + /// The "noshade" attribute. + /// + NoShade, + + /// + /// The "nowrap" attribute. + /// + NoWrap, + + /// + /// The "object" attribute. + /// + Object, + + /// + /// The "profile" attribute. + /// + Profile, + + /// + /// The "prompt" attribute. + /// + Prompt, + + /// + /// The "readonly" attribute. + /// + ReadOnly, + + /// + /// The "rel" attribute. + /// + Rel, + + /// + /// The "rev" attribute. + /// + Rev, + + /// + /// The "rows" attribute. + /// + Rows, + + /// + /// The "rowspan" attribute. + /// + RowSpan, + + /// + /// The "rules" attribute. + /// + Rules, + + /// + /// The "scheme" attribute. + /// + Scheme, + + /// + /// The "scope" attribute. + /// + Scope, + + /// + /// The "scrolling" attribute. + /// + Scrolling, + + /// + /// The "selected" attribute. + /// + Selected, + + /// + /// The "shape" attribute. + /// + Shape, + + /// + /// The "size" attribute. + /// + Size, + + /// + /// The "span" attribute. + /// + Span, + + /// + /// The "src" attribute. + /// + Src, + + /// + /// The "standby" attribute. + /// + StandBy, + + /// + /// The "start" attribute. + /// + Start, + + /// + /// The "style" attribute. + /// + Style, + + /// + /// The "summary" attribute. + /// + Summary, + + /// + /// The "tabindex" attribute. + /// + TabIndex, + + /// + /// The "target" attribute. + /// + Target, + + /// + /// The "text" attribute. + /// + Text, + + /// + /// The "title" attribute. + /// + Title, + + /// + /// The "topmargin" attribute. + /// + TopMargin, + + /// + /// The "type" attribute. + /// + Type, + + /// + /// The "usemap" attribute. + /// + UseMap, + + /// + /// The "valign" attribute. + /// + Valign, + + /// + /// The "value" attribute. + /// + Value, + + /// + /// The "valuetype" attribute. + /// + ValueType, + + /// + /// The "version" attribute. + /// + Version, + + /// + /// The "vlink" attribute. + /// + Vlink, + + /// + /// The "vspace" attribute. + /// + Vspace, + + /// + /// The "width" attribute. + /// + Width, + + /// + /// The "xmlns" attribute. + /// + XmlNS + } + + [AttributeUsage (AttributeTargets.Field)] + class HtmlAttributeNameAttribute : Attribute { + public HtmlAttributeNameAttribute (string name) + { + Name = name; + } + + public string Name { + get; protected set; + } + } + + /// + /// extension methods. + /// + /// + /// extension methods. + /// + public static class HtmlAttributeIdExtensions + { + static readonly Dictionary AttributeNameToId; + + static HtmlAttributeIdExtensions () + { + var values = (HtmlAttributeId[]) Enum.GetValues (typeof (HtmlAttributeId)); + + AttributeNameToId = new Dictionary (values.Length - 1, MimeUtils.OrdinalIgnoreCase); + + for (int i = 1; i < values.Length; i++) + AttributeNameToId.Add (values[i].ToAttributeName (), values[i]); + } + + /// + /// Converts the enum value into the equivalent attribute name. + /// + /// + /// Converts the enum value into the equivalent attribute name. + /// + /// The attribute name. + /// The enum value. + public static string ToAttributeName (this HtmlAttributeId value) + { + var name = value.ToString (); + +#if NETSTANDARD1_3 || NETSTANDARD1_6 + var field = typeof (HtmlAttributeId).GetTypeInfo ().GetDeclaredField (name); + var attrs = field.GetCustomAttributes (typeof (HtmlAttributeNameAttribute), false).ToArray (); +#else + var field = typeof (HtmlAttributeId).GetField (name); + var attrs = field.GetCustomAttributes (typeof (HtmlAttributeNameAttribute), false); +#endif + + if (attrs != null && attrs.Length == 1) + return ((HtmlAttributeNameAttribute) attrs[0]).Name; + + return name.ToLowerInvariant (); + } + + /// + /// Converts the attribute name into the equivalent attribute id. + /// + /// + /// Converts the attribute name into the equivalent attribute id. + /// + /// The attribute id. + /// The attribute name. + internal static HtmlAttributeId ToHtmlAttributeId (this string name) + { + HtmlAttributeId value; + + if (!AttributeNameToId.TryGetValue (name, out value)) + return HtmlAttributeId.Unknown; + + return value; + } + } +} diff --git a/src/MimeKit/Text/HtmlEntityDecoder.cs b/src/MimeKit/Text/HtmlEntityDecoder.cs new file mode 100644 index 0000000..3838bdc --- /dev/null +++ b/src/MimeKit/Text/HtmlEntityDecoder.cs @@ -0,0 +1,258 @@ +// +// HtmlEntityDecoder.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Text { + /// + /// An HTML entity decoder. + /// + /// + /// An HTML entity decoder. + /// + public partial class HtmlEntityDecoder + { + readonly char[] pushed; + readonly int[] states; + bool semicolon; + bool numeric; + byte digits; + byte xbase; + int index; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public HtmlEntityDecoder () + { + pushed = new char[MaxEntityLength]; + states = new int[MaxEntityLength]; + } + + bool PushNumericEntity (char c) + { + int v; + + if (xbase == 0) { + if (c == 'X' || c == 'x') { + states[index] = 0; + pushed[index] = c; + xbase = 16; + index++; + return true; + } + + xbase = 10; + } + + if (c <= '9') { + if (c < '0') + return false; + + v = c - '0'; + } else if (xbase == 16) { + if (c >= 'a') { + v = (c - 'a') + 10; + } else if (c >= 'A') { + v = (c - 'A') + 10; + } else { + return false; + } + } else { + return false; + } + + if (v >= (int) xbase) + return false; + + int state = states[index - 1]; + + // check for overflow + if (state > int.MaxValue / xbase) + return false; + + if (state == int.MaxValue / xbase && v > int.MaxValue % xbase) + return false; + + state = (state * xbase) + v; + states[index] = state; + pushed[index] = c; + digits++; + index++; + + return true; + } + + /// + /// Push the specified character into the HTML entity decoder. + /// + /// + /// Pushes the specified character into the HTML entity decoder. + /// The first character pushed MUST be the '&' character. + /// + /// true if the character was accepted; otherwise, false. + /// The character. + /// + /// is the first character being pushed and was not the '&' character. + /// + public bool Push (char c) + { + if (semicolon) + return false; + + if (index == 0) { + if (c != '&') + throw new ArgumentOutOfRangeException (nameof (c), "The first character that is pushed MUST be the '&' character."); + + pushed[index] = '&'; + states[index] = 0; + index++; + return true; + } + + if (index + 1 > MaxEntityLength) + return false; + + if (index == 1 && c == '#') { + pushed[index] = '#'; + states[index] = 0; + numeric = true; + index++; + return true; + } + + semicolon = c == ';'; + + if (numeric) { + if (c == ';') { + states[index] = states[index - 1]; + pushed[index] = ';'; + index++; + return true; + } + + return PushNumericEntity (c); + } + + return PushNamedEntity (c); + } + + string GetNumericEntityValue () + { + if (digits == 0 || !semicolon) + return new string (pushed, 0, index); + + int state = states[index - 1]; + + // the following states are parse errors + switch (state) { + case 0x00: return "\uFFFD"; // REPLACEMENT CHARACTER + case 0x80: return "\u20AC"; // EURO SIGN (€) + case 0x82: return "\u201A"; // SINGLE LOW-9 QUOTATION MARK (‚) + case 0x83: return "\u0192"; // LATIN SMALL LETTER F WITH HOOK (ƒ) + case 0x84: return "\u201E"; // DOUBLE LOW-9 QUOTATION MARK („) + case 0x85: return "\u2026"; // HORIZONTAL ELLIPSIS (…) + case 0x86: return "\u2020"; // DAGGER (†) + case 0x87: return "\u2021"; // DOUBLE DAGGER (‡) + case 0x88: return "\u02C6"; // MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ) + case 0x89: return "\u2030"; // PER MILLE SIGN (‰) + case 0x8A: return "\u0160"; // LATIN CAPITAL LETTER S WITH CARON (Š) + case 0x8B: return "\u2039"; // SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹) + case 0x8C: return "\u0152"; // LATIN CAPITAL LIGATURE OE (Œ) + case 0x8E: return "\u017D"; // LATIN CAPITAL LETTER Z WITH CARON (Ž) + case 0x91: return "\u2018"; // LEFT SINGLE QUOTATION MARK (‘) + case 0x92: return "\u2019"; // RIGHT SINGLE QUOTATION MARK (’) + case 0x93: return "\u201C"; // LEFT DOUBLE QUOTATION MARK (“) + case 0x94: return "\u201D"; // RIGHT DOUBLE QUOTATION MARK (”) + case 0x95: return "\u2022"; // BULLET (•) + case 0x96: return "\u2013"; // EN DASH (–) + case 0x97: return "\u2014"; // EM DASH (—) + case 0x98: return "\u02DC"; // SMALL TILDE (˜) + case 0x99: return "\u2122"; // TRADE MARK SIGN (™) + case 0x9A: return "\u0161"; // LATIN SMALL LETTER S WITH CARON (š) + case 0x9B: return "\u203A"; // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›) + case 0x9C: return "\u0153"; // LATIN SMALL LIGATURE OE (œ) + case 0x9E: return "\u017E"; // LATIN SMALL LETTER Z WITH CARON (ž) + case 0x9F: return "\u0178"; // LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ) + case 0x0000B: case 0x0FFFE: case 0x1FFFE: case 0x1FFFF: case 0x2FFFE: case 0x2FFFF: case 0x3FFFE: + case 0x3FFFF: case 0x4FFFE: case 0x4FFFF: case 0x5FFFE: case 0x5FFFF: case 0x6FFFE: case 0x6FFFF: + case 0x7FFFE: case 0x7FFFF: case 0x8FFFE: case 0x8FFFF: case 0x9FFFE: case 0x9FFFF: case 0xAFFFE: + case 0xAFFFF: case 0xBFFFE: case 0xBFFFF: case 0xCFFFE: case 0xCFFFF: case 0xDFFFE: case 0xDFFFF: + case 0xEFFFE: case 0xEFFFF: case 0xFFFFE: case 0xFFFFF: case 0x10FFFE: case 0x10FFFF: + // parse error + return new string (pushed, 0, index); + default: + if ((state >= 0xD800 && state <= 0xDFFF) || state > 0x10FFFF) { + // parse error, emit REPLACEMENT CHARACTER + return "\uFFFD"; + } + + if ((state >= 0x0001 && state <= 0x0008) || (state >= 0x000D && state <= 0x001F) || + (state >= 0x007F && state <= 0x009F) || (state >= 0xFDD0 && state <= 0xFDEF)) { + return new string (pushed, 0, index); + } + break; + } + + return char.ConvertFromUtf32 (state); + } + + /// + /// Get the decoded entity value. + /// + /// + /// Gets the decoded entity value. + /// + /// The value. + public string GetValue () + { + return numeric ? GetNumericEntityValue () : GetNamedEntityValue (); + } + + internal string GetPushedInput () + { + return new string (pushed, 0, index); + } + + /// + /// Reset the entity decoder. + /// + /// + /// Resets the entity decoder. + /// + public void Reset () + { + semicolon = false; + numeric = false; + digits = 0; + xbase = 0; + index = 0; + } + } +} diff --git a/src/MimeKit/Text/HtmlEntityDecoder.g.cs b/src/MimeKit/Text/HtmlEntityDecoder.g.cs new file mode 100644 index 0000000..f9cc9c9 --- /dev/null +++ b/src/MimeKit/Text/HtmlEntityDecoder.g.cs @@ -0,0 +1,12445 @@ +// +// HtmlEntityDecoder.g.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +// WARNING: This file is auto-generated. DO NOT EDIT! + +using System.Collections.Generic; + +namespace MimeKit.Text { + public partial class HtmlEntityDecoder + { + const int MaxEntityLength = 33; + + static readonly Dictionary NamedEntities; + + struct Transition + { + public readonly int From; + public readonly int To; + + public Transition (int from, int to) + { + From = from; + To = to; + } + } + + static readonly Transition[] TransitionTable_1; + static readonly Transition[] TransitionTable_2; + static readonly Transition[] TransitionTable_3; + static readonly Transition[] TransitionTable_4; + static readonly Transition[] TransitionTable_5; + static readonly Transition[] TransitionTable_6; + static readonly Transition[] TransitionTable_7; + static readonly Transition[] TransitionTable_8; + static readonly Transition[] TransitionTable_semicolon; + static readonly Transition[] TransitionTable_A; + static readonly Transition[] TransitionTable_B; + static readonly Transition[] TransitionTable_C; + static readonly Transition[] TransitionTable_D; + static readonly Transition[] TransitionTable_E; + static readonly Transition[] TransitionTable_F; + static readonly Transition[] TransitionTable_G; + static readonly Transition[] TransitionTable_H; + static readonly Transition[] TransitionTable_I; + static readonly Transition[] TransitionTable_J; + static readonly Transition[] TransitionTable_K; + static readonly Transition[] TransitionTable_L; + static readonly Transition[] TransitionTable_M; + static readonly Transition[] TransitionTable_N; + static readonly Transition[] TransitionTable_O; + static readonly Transition[] TransitionTable_P; + static readonly Transition[] TransitionTable_Q; + static readonly Transition[] TransitionTable_R; + static readonly Transition[] TransitionTable_S; + static readonly Transition[] TransitionTable_T; + static readonly Transition[] TransitionTable_U; + static readonly Transition[] TransitionTable_V; + static readonly Transition[] TransitionTable_W; + static readonly Transition[] TransitionTable_X; + static readonly Transition[] TransitionTable_Y; + static readonly Transition[] TransitionTable_Z; + static readonly Transition[] TransitionTable_a; + static readonly Transition[] TransitionTable_b; + static readonly Transition[] TransitionTable_c; + static readonly Transition[] TransitionTable_d; + static readonly Transition[] TransitionTable_e; + static readonly Transition[] TransitionTable_f; + static readonly Transition[] TransitionTable_g; + static readonly Transition[] TransitionTable_h; + static readonly Transition[] TransitionTable_i; + static readonly Transition[] TransitionTable_j; + static readonly Transition[] TransitionTable_k; + static readonly Transition[] TransitionTable_l; + static readonly Transition[] TransitionTable_m; + static readonly Transition[] TransitionTable_n; + static readonly Transition[] TransitionTable_o; + static readonly Transition[] TransitionTable_p; + static readonly Transition[] TransitionTable_q; + static readonly Transition[] TransitionTable_r; + static readonly Transition[] TransitionTable_s; + static readonly Transition[] TransitionTable_t; + static readonly Transition[] TransitionTable_u; + static readonly Transition[] TransitionTable_v; + static readonly Transition[] TransitionTable_w; + static readonly Transition[] TransitionTable_x; + static readonly Transition[] TransitionTable_y; + static readonly Transition[] TransitionTable_z; + + static HtmlEntityDecoder () + { + TransitionTable_1 = new Transition[4] { + new Transition (566, 567), // &blk -> &blk1 + new Transition (2280, 2282), // &emsp -> &emsp1 + new Transition (2649, 2650), // &frac -> &frac1 + new Transition (8284, 8286) // &sup -> ¹ + }; + TransitionTable_2 = new Transition[4] { + new Transition (567, 568), // &blk1 -> &blk12 + new Transition (2649, 2663), // &frac -> &frac2 + new Transition (2650, 2651), // &frac1 -> ½ + new Transition (8284, 8288) // &sup -> ² + }; + TransitionTable_3 = new Transition[6] { + new Transition (566, 572), // &blk -> &blk3 + new Transition (2282, 2283), // &emsp1 -> &emsp13 + new Transition (2649, 2668), // &frac -> &frac3 + new Transition (2650, 2653), // &frac1 -> &frac13 + new Transition (2663, 2664), // &frac2 -> &frac23 + new Transition (8284, 8290) // &sup -> ³ + }; + TransitionTable_4 = new Transition[7] { + new Transition (567, 570), // &blk1 -> &blk14 + new Transition (572, 573), // &blk3 -> &blk34 + new Transition (2282, 2285), // &emsp1 -> &emsp14 + new Transition (2649, 2675), // &frac -> &frac4 + new Transition (2650, 2655), // &frac1 -> ¼ + new Transition (2668, 2669), // &frac3 -> ¾ + new Transition (8464, 8465) // &there -> &there4 + }; + TransitionTable_5 = new Transition[5] { + new Transition (2649, 2678), // &frac -> &frac5 + new Transition (2650, 2657), // &frac1 -> &frac15 + new Transition (2663, 2666), // &frac2 -> &frac25 + new Transition (2668, 2671), // &frac3 -> &frac35 + new Transition (2675, 2676) // &frac4 -> &frac45 + }; + TransitionTable_6 = new Transition[2] { + new Transition (2650, 2659), // &frac1 -> &frac16 + new Transition (2678, 2679) // &frac5 -> &frac56 + }; + TransitionTable_7 = new Transition[1] { + new Transition (2649, 2683) // &frac -> &frac7 + }; + TransitionTable_8 = new Transition[4] { + new Transition (2650, 2661), // &frac1 -> &frac18 + new Transition (2668, 2673), // &frac3 -> &frac38 + new Transition (2678, 2681), // &frac5 -> &frac58 + new Transition (2683, 2684) // &frac7 -> &frac78 + }; + TransitionTable_semicolon = new Transition[2125] { + new Transition (6, 7), // Á -> Á + new Transition (13, 14), // á -> á + new Transition (19, 20), // &Abreve -> Ă + new Transition (25, 26), // &abreve -> ă + new Transition (27, 28), // &ac -> ∾ + new Transition (29, 30), // &acd -> ∿ + new Transition (31, 32), // &acE -> ∾̳ + new Transition (36, 37), //  ->  + new Transition (40, 41), // â -> â + new Transition (44, 45), // ´ -> ´ + new Transition (46, 47), // &Acy -> А + new Transition (48, 49), // &acy -> а + new Transition (53, 54), // Æ -> Æ + new Transition (58, 59), // æ -> æ + new Transition (60, 61), // &af -> ⁡ + new Transition (63, 64), // &Afr -> 𝔄 + new Transition (65, 66), // &afr -> 𝔞 + new Transition (71, 72), // À -> À + new Transition (77, 78), // à -> à + new Transition (84, 85), // &alefsym -> ℵ + new Transition (87, 88), // &aleph -> ℵ + new Transition (92, 93), // &Alpha -> Α + new Transition (96, 97), // &alpha -> α + new Transition (101, 102), // &Amacr -> Ā + new Transition (106, 107), // &amacr -> ā + new Transition (109, 110), // &amalg -> ⨿ + new Transition (112, 113), // & -> & + new Transition (114, 115), // & -> & + new Transition (117, 118), // &And -> ⩓ + new Transition (120, 121), // &and -> ∧ + new Transition (124, 125), // &andand -> ⩕ + new Transition (126, 127), // &andd -> ⩜ + new Transition (132, 133), // &andslope -> ⩘ + new Transition (134, 135), // &andv -> ⩚ + new Transition (136, 137), // &ang -> ∠ + new Transition (138, 139), // &ange -> ⦤ + new Transition (141, 142), // &angle -> ∠ + new Transition (145, 146), // &angmsd -> ∡ + new Transition (148, 149), // &angmsdaa -> ⦨ + new Transition (150, 151), // &angmsdab -> ⦩ + new Transition (152, 153), // &angmsdac -> ⦪ + new Transition (154, 155), // &angmsdad -> ⦫ + new Transition (156, 157), // &angmsdae -> ⦬ + new Transition (158, 159), // &angmsdaf -> ⦭ + new Transition (160, 161), // &angmsdag -> ⦮ + new Transition (162, 163), // &angmsdah -> ⦯ + new Transition (165, 166), // &angrt -> ∟ + new Transition (168, 169), // &angrtvb -> ⊾ + new Transition (170, 171), // &angrtvbd -> ⦝ + new Transition (174, 175), // &angsph -> ∢ + new Transition (176, 177), // &angst -> Å + new Transition (181, 182), // &angzarr -> ⍼ + new Transition (186, 187), // &Aogon -> Ą + new Transition (191, 192), // &aogon -> ą + new Transition (194, 195), // &Aopf -> 𝔸 + new Transition (197, 198), // &aopf -> 𝕒 + new Transition (199, 200), // &ap -> ≈ + new Transition (204, 205), // &apacir -> ⩯ + new Transition (206, 207), // &apE -> ⩰ + new Transition (208, 209), // &ape -> ≊ + new Transition (211, 212), // &apid -> ≋ + new Transition (214, 215), // &apos -> ' + new Transition (227, 228), // &ApplyFunction -> ⁡ + new Transition (232, 233), // &approx -> ≈ + new Transition (235, 236), // &approxeq -> ≊ + new Transition (240, 241), // Å -> Å + new Transition (245, 246), // å -> å + new Transition (249, 250), // &Ascr -> 𝒜 + new Transition (253, 254), // &ascr -> 𝒶 + new Transition (258, 259), // &Assign -> ≔ + new Transition (260, 261), // &ast -> * + new Transition (264, 265), // &asymp -> ≈ + new Transition (267, 268), // &asympeq -> ≍ + new Transition (273, 274), // à -> à + new Transition (279, 280), // ã -> ã + new Transition (283, 284), // Ä -> Ä + new Transition (287, 288), // ä -> ä + new Transition (295, 296), // &awconint -> ∳ + new Transition (299, 300), // &awint -> ⨑ + new Transition (308, 309), // &backcong -> ≌ + new Transition (316, 317), // &backepsilon -> ϶ + new Transition (322, 323), // &backprime -> ‵ + new Transition (326, 327), // &backsim -> ∽ + new Transition (329, 330), // &backsimeq -> ⋍ + new Transition (339, 340), // &Backslash -> ∖ + new Transition (342, 343), // &Barv -> ⫧ + new Transition (347, 348), // &barvee -> ⊽ + new Transition (351, 352), // &Barwed -> ⌆ + new Transition (355, 356), // &barwed -> ⌅ + new Transition (358, 359), // &barwedge -> ⌅ + new Transition (362, 363), // &bbrk -> ⎵ + new Transition (367, 368), // &bbrktbrk -> ⎶ + new Transition (372, 373), // &bcong -> ≌ + new Transition (375, 376), // &Bcy -> Б + new Transition (377, 378), // &bcy -> б + new Transition (382, 383), // &bdquo -> „ + new Transition (388, 389), // &becaus -> ∵ + new Transition (395, 396), // &Because -> ∵ + new Transition (397, 398), // &because -> ∵ + new Transition (403, 404), // &bemptyv -> ⦰ + new Transition (407, 408), // &bepsi -> ϶ + new Transition (412, 413), // &bernou -> ℬ + new Transition (421, 422), // &Bernoullis -> ℬ + new Transition (424, 425), // &Beta -> Β + new Transition (427, 428), // &beta -> β + new Transition (429, 430), // &beth -> ℶ + new Transition (434, 435), // &between -> ≬ + new Transition (437, 438), // &Bfr -> 𝔅 + new Transition (440, 441), // &bfr -> 𝔟 + new Transition (446, 447), // &bigcap -> ⋂ + new Transition (450, 451), // &bigcirc -> ◯ + new Transition (453, 454), // &bigcup -> ⋃ + new Transition (458, 459), // &bigodot -> ⨀ + new Transition (463, 464), // &bigoplus -> ⨁ + new Transition (469, 470), // &bigotimes -> ⨂ + new Transition (475, 476), // &bigsqcup -> ⨆ + new Transition (479, 480), // &bigstar -> ★ + new Transition (492, 493), // &bigtriangledown -> ▽ + new Transition (495, 496), // &bigtriangleup -> △ + new Transition (501, 502), // &biguplus -> ⨄ + new Transition (505, 506), // &bigvee -> ⋁ + new Transition (511, 512), // &bigwedge -> ⋀ + new Transition (517, 518), // &bkarow -> ⤍ + new Transition (529, 530), // &blacklozenge -> ⧫ + new Transition (536, 537), // &blacksquare -> ▪ + new Transition (545, 546), // &blacktriangle -> ▴ + new Transition (550, 551), // &blacktriangledown -> ▾ + new Transition (555, 556), // &blacktriangleleft -> ◂ + new Transition (561, 562), // &blacktriangleright -> ▸ + new Transition (564, 565), // &blank -> ␣ + new Transition (568, 569), // &blk12 -> ▒ + new Transition (570, 571), // &blk14 -> ░ + new Transition (573, 574), // &blk34 -> ▓ + new Transition (577, 578), // &block -> █ + new Transition (580, 581), // &bne -> =⃥ + new Transition (585, 586), // &bnequiv -> ≡⃥ + new Transition (589, 590), // &bNot -> ⫭ + new Transition (592, 593), // &bnot -> ⌐ + new Transition (596, 597), // &Bopf -> 𝔹 + new Transition (600, 601), // &bopf -> 𝕓 + new Transition (602, 603), // &bot -> ⊥ + new Transition (606, 607), // &bottom -> ⊥ + new Transition (611, 612), // &bowtie -> ⋈ + new Transition (616, 617), // &boxbox -> ⧉ + new Transition (619, 620), // &boxDL -> ╗ + new Transition (621, 622), // &boxDl -> ╖ + new Transition (624, 625), // &boxdL -> ╕ + new Transition (626, 627), // &boxdl -> ┐ + new Transition (628, 629), // &boxDR -> ╔ + new Transition (630, 631), // &boxDr -> ╓ + new Transition (632, 633), // &boxdR -> ╒ + new Transition (634, 635), // &boxdr -> ┌ + new Transition (636, 637), // &boxH -> ═ + new Transition (638, 639), // &boxh -> ─ + new Transition (640, 641), // &boxHD -> ╦ + new Transition (642, 643), // &boxHd -> ╤ + new Transition (644, 645), // &boxhD -> ╥ + new Transition (646, 647), // &boxhd -> ┬ + new Transition (648, 649), // &boxHU -> ╩ + new Transition (650, 651), // &boxHu -> ╧ + new Transition (652, 653), // &boxhU -> ╨ + new Transition (654, 655), // &boxhu -> ┴ + new Transition (660, 661), // &boxminus -> ⊟ + new Transition (665, 666), // &boxplus -> ⊞ + new Transition (671, 672), // &boxtimes -> ⊠ + new Transition (674, 675), // &boxUL -> ╝ + new Transition (676, 677), // &boxUl -> ╜ + new Transition (679, 680), // &boxuL -> ╛ + new Transition (681, 682), // &boxul -> ┘ + new Transition (683, 684), // &boxUR -> ╚ + new Transition (685, 686), // &boxUr -> ╙ + new Transition (687, 688), // &boxuR -> ╘ + new Transition (689, 690), // &boxur -> └ + new Transition (691, 692), // &boxV -> ║ + new Transition (693, 694), // &boxv -> │ + new Transition (695, 696), // &boxVH -> ╬ + new Transition (697, 698), // &boxVh -> ╫ + new Transition (699, 700), // &boxvH -> ╪ + new Transition (701, 702), // &boxvh -> ┼ + new Transition (703, 704), // &boxVL -> ╣ + new Transition (705, 706), // &boxVl -> ╢ + new Transition (707, 708), // &boxvL -> ╡ + new Transition (709, 710), // &boxvl -> ┤ + new Transition (711, 712), // &boxVR -> ╠ + new Transition (713, 714), // &boxVr -> ╟ + new Transition (715, 716), // &boxvR -> ╞ + new Transition (717, 718), // &boxvr -> ├ + new Transition (723, 724), // &bprime -> ‵ + new Transition (728, 729), // &Breve -> ˘ + new Transition (733, 734), // &breve -> ˘ + new Transition (738, 739), // ¦ -> ¦ + new Transition (742, 743), // &Bscr -> ℬ + new Transition (746, 747), // &bscr -> 𝒷 + new Transition (750, 751), // &bsemi -> ⁏ + new Transition (753, 754), // &bsim -> ∽ + new Transition (755, 756), // &bsime -> ⋍ + new Transition (758, 759), // &bsol -> \ + new Transition (760, 761), // &bsolb -> ⧅ + new Transition (765, 766), // &bsolhsub -> ⟈ + new Transition (769, 770), // &bull -> • + new Transition (772, 773), // &bullet -> • + new Transition (775, 776), // &bump -> ≎ + new Transition (777, 778), // &bumpE -> ⪮ + new Transition (779, 780), // &bumpe -> ≏ + new Transition (785, 786), // &Bumpeq -> ≎ + new Transition (787, 788), // &bumpeq -> ≏ + new Transition (794, 795), // &Cacute -> Ć + new Transition (801, 802), // &cacute -> ć + new Transition (803, 804), // &Cap -> ⋒ + new Transition (805, 806), // &cap -> ∩ + new Transition (809, 810), // &capand -> ⩄ + new Transition (815, 816), // &capbrcup -> ⩉ + new Transition (819, 820), // &capcap -> ⩋ + new Transition (822, 823), // &capcup -> ⩇ + new Transition (826, 827), // &capdot -> ⩀ + new Transition (844, 845), // &CapitalDifferentialD -> ⅅ + new Transition (846, 847), // &caps -> ∩︀ + new Transition (850, 851), // &caret -> ⁁ + new Transition (853, 854), // &caron -> ˇ + new Transition (859, 860), // &Cayleys -> ℭ + new Transition (864, 865), // &ccaps -> ⩍ + new Transition (870, 871), // &Ccaron -> Č + new Transition (874, 875), // &ccaron -> č + new Transition (879, 880), // Ç -> Ç + new Transition (884, 885), // ç -> ç + new Transition (888, 889), // &Ccirc -> Ĉ + new Transition (892, 893), // &ccirc -> ĉ + new Transition (898, 899), // &Cconint -> ∰ + new Transition (902, 903), // &ccups -> ⩌ + new Transition (905, 906), // &ccupssm -> ⩐ + new Transition (909, 910), // &Cdot -> Ċ + new Transition (913, 914), // &cdot -> ċ + new Transition (918, 919), // ¸ -> ¸ + new Transition (925, 926), // &Cedilla -> ¸ + new Transition (931, 932), // &cemptyv -> ⦲ + new Transition (934, 935), // ¢ -> ¢ + new Transition (942, 943), // &CenterDot -> · + new Transition (948, 949), // ¢erdot -> · + new Transition (951, 952), // &Cfr -> ℭ + new Transition (954, 955), // &cfr -> 𝔠 + new Transition (958, 959), // &CHcy -> Ч + new Transition (962, 963), // &chcy -> ч + new Transition (966, 967), // &check -> ✓ + new Transition (971, 972), // &checkmark -> ✓ + new Transition (974, 975), // &Chi -> Χ + new Transition (976, 977), // &chi -> χ + new Transition (979, 980), // &cir -> ○ + new Transition (981, 982), // &circ -> ˆ + new Transition (984, 985), // &circeq -> ≗ + new Transition (996, 997), // &circlearrowleft -> ↺ + new Transition (1002, 1003), // &circlearrowright -> ↻ + new Transition (1007, 1008), // &circledast -> ⊛ + new Transition (1012, 1013), // &circledcirc -> ⊚ + new Transition (1017, 1018), // &circleddash -> ⊝ + new Transition (1026, 1027), // &CircleDot -> ⊙ + new Transition (1028, 1029), // &circledR -> ® + new Transition (1030, 1031), // &circledS -> Ⓢ + new Transition (1036, 1037), // &CircleMinus -> ⊖ + new Transition (1041, 1042), // &CirclePlus -> ⊕ + new Transition (1047, 1048), // &CircleTimes -> ⊗ + new Transition (1049, 1050), // &cirE -> ⧃ + new Transition (1051, 1052), // &cire -> ≗ + new Transition (1057, 1058), // &cirfnint -> ⨐ + new Transition (1061, 1062), // &cirmid -> ⫯ + new Transition (1066, 1067), // &cirscir -> ⧂ + new Transition (1090, 1091), // &ClockwiseContourIntegral -> ∲ + new Transition (1109, 1110), // &CloseCurlyDoubleQuote -> ” + new Transition (1115, 1116), // &CloseCurlyQuote -> ’ + new Transition (1120, 1121), // &clubs -> ♣ + new Transition (1124, 1125), // &clubsuit -> ♣ + new Transition (1129, 1130), // &Colon -> ∷ + new Transition (1134, 1135), // &colon -> : + new Transition (1136, 1137), // &Colone -> ⩴ + new Transition (1138, 1139), // &colone -> ≔ + new Transition (1140, 1141), // &coloneq -> ≔ + new Transition (1144, 1145), // &comma -> , + new Transition (1146, 1147), // &commat -> @ + new Transition (1148, 1149), // &comp -> ∁ + new Transition (1151, 1152), // &compfn -> ∘ + new Transition (1158, 1159), // &complement -> ∁ + new Transition (1162, 1163), // &complexes -> ℂ + new Transition (1165, 1166), // &cong -> ≅ + new Transition (1169, 1170), // &congdot -> ⩭ + new Transition (1177, 1178), // &Congruent -> ≡ + new Transition (1181, 1182), // &Conint -> ∯ + new Transition (1185, 1186), // &conint -> ∮ + new Transition (1198, 1199), // &ContourIntegral -> ∮ + new Transition (1201, 1202), // &Copf -> ℂ + new Transition (1204, 1205), // &copf -> 𝕔 + new Transition (1208, 1209), // &coprod -> ∐ + new Transition (1215, 1216), // &Coproduct -> ∐ + new Transition (1219, 1220), // © -> © + new Transition (1221, 1222), // © -> © + new Transition (1224, 1225), // ©sr -> ℗ + new Transition (1254, 1255), // &CounterClockwiseContourIntegral -> ∳ + new Transition (1259, 1260), // &crarr -> ↵ + new Transition (1264, 1265), // &Cross -> ⨯ + new Transition (1268, 1269), // &cross -> ✗ + new Transition (1272, 1273), // &Cscr -> 𝒞 + new Transition (1276, 1277), // &cscr -> 𝒸 + new Transition (1279, 1280), // &csub -> ⫏ + new Transition (1281, 1282), // &csube -> ⫑ + new Transition (1283, 1284), // &csup -> ⫐ + new Transition (1285, 1286), // &csupe -> ⫒ + new Transition (1290, 1291), // &ctdot -> ⋯ + new Transition (1297, 1298), // &cudarrl -> ⤸ + new Transition (1299, 1300), // &cudarrr -> ⤵ + new Transition (1303, 1304), // &cuepr -> ⋞ + new Transition (1306, 1307), // &cuesc -> ⋟ + new Transition (1311, 1312), // &cularr -> ↶ + new Transition (1313, 1314), // &cularrp -> ⤽ + new Transition (1316, 1317), // &Cup -> ⋓ + new Transition (1318, 1319), // &cup -> ∪ + new Transition (1324, 1325), // &cupbrcap -> ⩈ + new Transition (1328, 1329), // &CupCap -> ≍ + new Transition (1332, 1333), // &cupcap -> ⩆ + new Transition (1335, 1336), // &cupcup -> ⩊ + new Transition (1339, 1340), // &cupdot -> ⊍ + new Transition (1342, 1343), // &cupor -> ⩅ + new Transition (1344, 1345), // &cups -> ∪︀ + new Transition (1349, 1350), // &curarr -> ↷ + new Transition (1351, 1352), // &curarrm -> ⤼ + new Transition (1360, 1361), // &curlyeqprec -> ⋞ + new Transition (1365, 1366), // &curlyeqsucc -> ⋟ + new Transition (1369, 1370), // &curlyvee -> ⋎ + new Transition (1375, 1376), // &curlywedge -> ⋏ + new Transition (1379, 1380), // ¤ -> ¤ + new Transition (1391, 1392), // &curvearrowleft -> ↶ + new Transition (1397, 1398), // &curvearrowright -> ↷ + new Transition (1401, 1402), // &cuvee -> ⋎ + new Transition (1405, 1406), // &cuwed -> ⋏ + new Transition (1413, 1414), // &cwconint -> ∲ + new Transition (1417, 1418), // &cwint -> ∱ + new Transition (1423, 1424), // &cylcty -> ⌭ + new Transition (1430, 1431), // &Dagger -> ‡ + new Transition (1437, 1438), // &dagger -> † + new Transition (1442, 1443), // &daleth -> ℸ + new Transition (1445, 1446), // &Darr -> ↡ + new Transition (1449, 1450), // &dArr -> ⇓ + new Transition (1452, 1453), // &darr -> ↓ + new Transition (1455, 1456), // &dash -> ‐ + new Transition (1459, 1460), // &Dashv -> ⫤ + new Transition (1461, 1462), // &dashv -> ⊣ + new Transition (1468, 1469), // &dbkarow -> ⤏ + new Transition (1472, 1473), // &dblac -> ˝ + new Transition (1478, 1479), // &Dcaron -> Ď + new Transition (1484, 1485), // &dcaron -> ď + new Transition (1486, 1487), // &Dcy -> Д + new Transition (1488, 1489), // &dcy -> д + new Transition (1490, 1491), // &DD -> ⅅ + new Transition (1492, 1493), // &dd -> ⅆ + new Transition (1498, 1499), // &ddagger -> ‡ + new Transition (1501, 1502), // &ddarr -> ⇊ + new Transition (1508, 1509), // &DDotrahd -> ⤑ + new Transition (1514, 1515), // &ddotseq -> ⩷ + new Transition (1517, 1518), // ° -> ° + new Transition (1520, 1521), // &Del -> ∇ + new Transition (1523, 1524), // &Delta -> Δ + new Transition (1527, 1528), // &delta -> δ + new Transition (1533, 1534), // &demptyv -> ⦱ + new Transition (1539, 1540), // &dfisht -> ⥿ + new Transition (1542, 1543), // &Dfr -> 𝔇 + new Transition (1544, 1545), // &dfr -> 𝔡 + new Transition (1548, 1549), // &dHar -> ⥥ + new Transition (1553, 1554), // &dharl -> ⇃ + new Transition (1555, 1556), // &dharr -> ⇂ + new Transition (1571, 1572), // &DiacriticalAcute -> ´ + new Transition (1575, 1576), // &DiacriticalDot -> ˙ + new Transition (1585, 1586), // &DiacriticalDoubleAcute -> ˝ + new Transition (1591, 1592), // &DiacriticalGrave -> ` + new Transition (1597, 1598), // &DiacriticalTilde -> ˜ + new Transition (1601, 1602), // &diam -> ⋄ + new Transition (1606, 1607), // &Diamond -> ⋄ + new Transition (1610, 1611), // &diamond -> ⋄ + new Transition (1615, 1616), // &diamondsuit -> ♦ + new Transition (1617, 1618), // &diams -> ♦ + new Transition (1619, 1620), // &die -> ¨ + new Transition (1631, 1632), // &DifferentialD -> ⅆ + new Transition (1637, 1638), // &digamma -> ϝ + new Transition (1641, 1642), // &disin -> ⋲ + new Transition (1643, 1644), // &div -> ÷ + new Transition (1647, 1648), // ÷ -> ÷ + new Transition (1655, 1656), // ÷ontimes -> ⋇ + new Transition (1659, 1660), // &divonx -> ⋇ + new Transition (1663, 1664), // &DJcy -> Ђ + new Transition (1667, 1668), // &djcy -> ђ + new Transition (1673, 1674), // &dlcorn -> ⌞ + new Transition (1677, 1678), // &dlcrop -> ⌍ + new Transition (1683, 1684), // &dollar -> $ + new Transition (1687, 1688), // &Dopf -> 𝔻 + new Transition (1690, 1691), // &dopf -> 𝕕 + new Transition (1692, 1693), // &Dot -> ¨ + new Transition (1694, 1695), // &dot -> ˙ + new Transition (1698, 1699), // &DotDot -> ⃜ + new Transition (1701, 1702), // &doteq -> ≐ + new Transition (1705, 1706), // &doteqdot -> ≑ + new Transition (1711, 1712), // &DotEqual -> ≐ + new Transition (1717, 1718), // &dotminus -> ∸ + new Transition (1722, 1723), // &dotplus -> ∔ + new Transition (1729, 1730), // &dotsquare -> ⊡ + new Transition (1742, 1743), // &doublebarwedge -> ⌆ + new Transition (1762, 1763), // &DoubleContourIntegral -> ∯ + new Transition (1766, 1767), // &DoubleDot -> ¨ + new Transition (1774, 1775), // &DoubleDownArrow -> ⇓ + new Transition (1784, 1785), // &DoubleLeftArrow -> ⇐ + new Transition (1795, 1796), // &DoubleLeftRightArrow -> ⇔ + new Transition (1799, 1800), // &DoubleLeftTee -> ⫤ + new Transition (1812, 1813), // &DoubleLongLeftArrow -> ⟸ + new Transition (1823, 1824), // &DoubleLongLeftRightArrow -> ⟺ + new Transition (1834, 1835), // &DoubleLongRightArrow -> ⟹ + new Transition (1845, 1846), // &DoubleRightArrow -> ⇒ + new Transition (1849, 1850), // &DoubleRightTee -> ⊨ + new Transition (1857, 1858), // &DoubleUpArrow -> ⇑ + new Transition (1867, 1868), // &DoubleUpDownArrow -> ⇕ + new Transition (1879, 1880), // &DoubleVerticalBar -> ∥ + new Transition (1887, 1888), // &DownArrow -> ↓ + new Transition (1893, 1894), // &Downarrow -> ⇓ + new Transition (1901, 1902), // &downarrow -> ↓ + new Transition (1905, 1906), // &DownArrowBar -> ⤓ + new Transition (1913, 1914), // &DownArrowUpArrow -> ⇵ + new Transition (1919, 1920), // &DownBreve -> ̑ + new Transition (1930, 1931), // &downdownarrows -> ⇊ + new Transition (1942, 1943), // &downharpoonleft -> ⇃ + new Transition (1948, 1949), // &downharpoonright -> ⇂ + new Transition (1964, 1965), // &DownLeftRightVector -> ⥐ + new Transition (1974, 1975), // &DownLeftTeeVector -> ⥞ + new Transition (1981, 1982), // &DownLeftVector -> ↽ + new Transition (1985, 1986), // &DownLeftVectorBar -> ⥖ + new Transition (2000, 2001), // &DownRightTeeVector -> ⥟ + new Transition (2007, 2008), // &DownRightVector -> ⇁ + new Transition (2011, 2012), // &DownRightVectorBar -> ⥗ + new Transition (2015, 2016), // &DownTee -> ⊤ + new Transition (2021, 2022), // &DownTeeArrow -> ↧ + new Transition (2029, 2030), // &drbkarow -> ⤐ + new Transition (2034, 2035), // &drcorn -> ⌟ + new Transition (2038, 2039), // &drcrop -> ⌌ + new Transition (2042, 2043), // &Dscr -> 𝒟 + new Transition (2046, 2047), // &dscr -> 𝒹 + new Transition (2050, 2051), // &DScy -> Ѕ + new Transition (2052, 2053), // &dscy -> ѕ + new Transition (2055, 2056), // &dsol -> ⧶ + new Transition (2060, 2061), // &Dstrok -> Đ + new Transition (2065, 2066), // &dstrok -> đ + new Transition (2070, 2071), // &dtdot -> ⋱ + new Transition (2073, 2074), // &dtri -> ▿ + new Transition (2075, 2076), // &dtrif -> ▾ + new Transition (2080, 2081), // &duarr -> ⇵ + new Transition (2084, 2085), // &duhar -> ⥯ + new Transition (2091, 2092), // &dwangle -> ⦦ + new Transition (2095, 2096), // &DZcy -> Џ + new Transition (2099, 2100), // &dzcy -> џ + new Transition (2106, 2107), // &dzigrarr -> ⟿ + new Transition (2113, 2114), // É -> É + new Transition (2120, 2121), // é -> é + new Transition (2125, 2126), // &easter -> ⩮ + new Transition (2131, 2132), // &Ecaron -> Ě + new Transition (2137, 2138), // &ecaron -> ě + new Transition (2140, 2141), // &ecir -> ≖ + new Transition (2144, 2145), // Ê -> Ê + new Transition (2146, 2147), // ê -> ê + new Transition (2151, 2152), // &ecolon -> ≕ + new Transition (2153, 2154), // &Ecy -> Э + new Transition (2155, 2156), // &ecy -> э + new Transition (2160, 2161), // &eDDot -> ⩷ + new Transition (2164, 2165), // &Edot -> Ė + new Transition (2167, 2168), // &eDot -> ≑ + new Transition (2171, 2172), // &edot -> ė + new Transition (2173, 2174), // &ee -> ⅇ + new Transition (2178, 2179), // &efDot -> ≒ + new Transition (2181, 2182), // &Efr -> 𝔈 + new Transition (2183, 2184), // &efr -> 𝔢 + new Transition (2185, 2186), // &eg -> ⪚ + new Transition (2191, 2192), // È -> È + new Transition (2196, 2197), // è -> è + new Transition (2198, 2199), // &egs -> ⪖ + new Transition (2202, 2203), // &egsdot -> ⪘ + new Transition (2204, 2205), // &el -> ⪙ + new Transition (2211, 2212), // &Element -> ∈ + new Transition (2218, 2219), // &elinters -> ⏧ + new Transition (2220, 2221), // &ell -> ℓ + new Transition (2222, 2223), // &els -> ⪕ + new Transition (2226, 2227), // &elsdot -> ⪗ + new Transition (2231, 2232), // &Emacr -> Ē + new Transition (2236, 2237), // &emacr -> ē + new Transition (2240, 2241), // &empty -> ∅ + new Transition (2244, 2245), // &emptyset -> ∅ + new Transition (2259, 2260), // &EmptySmallSquare -> ◻ + new Transition (2261, 2262), // &emptyv -> ∅ + new Transition (2277, 2278), // &EmptyVerySmallSquare -> ▫ + new Transition (2280, 2281), // &emsp ->   + new Transition (2283, 2284), // &emsp13 ->   + new Transition (2285, 2286), // &emsp14 ->   + new Transition (2288, 2289), // &ENG -> Ŋ + new Transition (2291, 2292), // &eng -> ŋ + new Transition (2294, 2295), // &ensp ->   + new Transition (2299, 2300), // &Eogon -> Ę + new Transition (2304, 2305), // &eogon -> ę + new Transition (2307, 2308), // &Eopf -> 𝔼 + new Transition (2310, 2311), // &eopf -> 𝕖 + new Transition (2314, 2315), // &epar -> ⋕ + new Transition (2317, 2318), // &eparsl -> ⧣ + new Transition (2321, 2322), // &eplus -> ⩱ + new Transition (2324, 2325), // &epsi -> ε + new Transition (2331, 2332), // &Epsilon -> Ε + new Transition (2335, 2336), // &epsilon -> ε + new Transition (2337, 2338), // &epsiv -> ϵ + new Transition (2343, 2344), // &eqcirc -> ≖ + new Transition (2348, 2349), // &eqcolon -> ≕ + new Transition (2352, 2353), // &eqsim -> ≂ + new Transition (2360, 2361), // &eqslantgtr -> ⪖ + new Transition (2365, 2366), // &eqslantless -> ⪕ + new Transition (2370, 2371), // &Equal -> ⩵ + new Transition (2375, 2376), // &equals -> = + new Transition (2381, 2382), // &EqualTilde -> ≂ + new Transition (2385, 2386), // &equest -> ≟ + new Transition (2394, 2395), // &Equilibrium -> ⇌ + new Transition (2397, 2398), // &equiv -> ≡ + new Transition (2400, 2401), // &equivDD -> ⩸ + new Transition (2407, 2408), // &eqvparsl -> ⧥ + new Transition (2412, 2413), // &erarr -> ⥱ + new Transition (2416, 2417), // &erDot -> ≓ + new Transition (2420, 2421), // &Escr -> ℰ + new Transition (2424, 2425), // &escr -> ℯ + new Transition (2428, 2429), // &esdot -> ≐ + new Transition (2431, 2432), // &Esim -> ⩳ + new Transition (2434, 2435), // &esim -> ≂ + new Transition (2437, 2438), // &Eta -> Η + new Transition (2440, 2441), // &eta -> η + new Transition (2443, 2444), // Ð -> Ð + new Transition (2445, 2446), // ð -> ð + new Transition (2449, 2450), // Ë -> Ë + new Transition (2453, 2454), // ë -> ë + new Transition (2456, 2457), // &euro -> € + new Transition (2460, 2461), // &excl -> ! + new Transition (2464, 2465), // &exist -> ∃ + new Transition (2470, 2471), // &Exists -> ∃ + new Transition (2480, 2481), // &expectation -> ℰ + new Transition (2491, 2492), // &ExponentialE -> ⅇ + new Transition (2501, 2502), // &exponentiale -> ⅇ + new Transition (2515, 2516), // &fallingdotseq -> ≒ + new Transition (2519, 2520), // &Fcy -> Ф + new Transition (2522, 2523), // &fcy -> ф + new Transition (2528, 2529), // &female -> ♀ + new Transition (2534, 2535), // &ffilig -> ffi + new Transition (2538, 2539), // &fflig -> ff + new Transition (2542, 2543), // &ffllig -> ffl + new Transition (2545, 2546), // &Ffr -> 𝔉 + new Transition (2547, 2548), // &ffr -> 𝔣 + new Transition (2552, 2553), // &filig -> fi + new Transition (2569, 2570), // &FilledSmallSquare -> ◼ + new Transition (2585, 2586), // &FilledVerySmallSquare -> ▪ + new Transition (2590, 2591), // &fjlig -> fj + new Transition (2594, 2595), // &flat -> ♭ + new Transition (2598, 2599), // &fllig -> fl + new Transition (2602, 2603), // &fltns -> ▱ + new Transition (2606, 2607), // &fnof -> ƒ + new Transition (2610, 2611), // &Fopf -> 𝔽 + new Transition (2614, 2615), // &fopf -> 𝕗 + new Transition (2619, 2620), // &ForAll -> ∀ + new Transition (2624, 2625), // &forall -> ∀ + new Transition (2626, 2627), // &fork -> ⋔ + new Transition (2628, 2629), // &forkv -> ⫙ + new Transition (2637, 2638), // &Fouriertrf -> ℱ + new Transition (2645, 2646), // &fpartint -> ⨍ + new Transition (2651, 2652), // ½ -> ½ + new Transition (2653, 2654), // &frac13 -> ⅓ + new Transition (2655, 2656), // ¼ -> ¼ + new Transition (2657, 2658), // &frac15 -> ⅕ + new Transition (2659, 2660), // &frac16 -> ⅙ + new Transition (2661, 2662), // &frac18 -> ⅛ + new Transition (2664, 2665), // &frac23 -> ⅔ + new Transition (2666, 2667), // &frac25 -> ⅖ + new Transition (2669, 2670), // ¾ -> ¾ + new Transition (2671, 2672), // &frac35 -> ⅗ + new Transition (2673, 2674), // &frac38 -> ⅜ + new Transition (2676, 2677), // &frac45 -> ⅘ + new Transition (2679, 2680), // &frac56 -> ⅚ + new Transition (2681, 2682), // &frac58 -> ⅝ + new Transition (2684, 2685), // &frac78 -> ⅞ + new Transition (2687, 2688), // &frasl -> ⁄ + new Transition (2691, 2692), // &frown -> ⌢ + new Transition (2695, 2696), // &Fscr -> ℱ + new Transition (2699, 2700), // &fscr -> 𝒻 + new Transition (2706, 2707), // &gacute -> ǵ + new Transition (2712, 2713), // &Gamma -> Γ + new Transition (2716, 2717), // &gamma -> γ + new Transition (2718, 2719), // &Gammad -> Ϝ + new Transition (2720, 2721), // &gammad -> ϝ + new Transition (2722, 2723), // &gap -> ⪆ + new Transition (2728, 2729), // &Gbreve -> Ğ + new Transition (2734, 2735), // &gbreve -> ğ + new Transition (2740, 2741), // &Gcedil -> Ģ + new Transition (2744, 2745), // &Gcirc -> Ĝ + new Transition (2749, 2750), // &gcirc -> ĝ + new Transition (2751, 2752), // &Gcy -> Г + new Transition (2753, 2754), // &gcy -> г + new Transition (2757, 2758), // &Gdot -> Ġ + new Transition (2761, 2762), // &gdot -> ġ + new Transition (2763, 2764), // &gE -> ≧ + new Transition (2765, 2766), // &ge -> ≥ + new Transition (2767, 2768), // &gEl -> ⪌ + new Transition (2769, 2770), // &gel -> ⋛ + new Transition (2771, 2772), // &geq -> ≥ + new Transition (2773, 2774), // &geqq -> ≧ + new Transition (2779, 2780), // &geqslant -> ⩾ + new Transition (2781, 2782), // &ges -> ⩾ + new Transition (2784, 2785), // &gescc -> ⪩ + new Transition (2788, 2789), // &gesdot -> ⪀ + new Transition (2790, 2791), // &gesdoto -> ⪂ + new Transition (2792, 2793), // &gesdotol -> ⪄ + new Transition (2794, 2795), // &gesl -> ⋛︀ + new Transition (2797, 2798), // &gesles -> ⪔ + new Transition (2800, 2801), // &Gfr -> 𝔊 + new Transition (2803, 2804), // &gfr -> 𝔤 + new Transition (2805, 2806), // &Gg -> ⋙ + new Transition (2807, 2808), // &gg -> ≫ + new Transition (2809, 2810), // &ggg -> ⋙ + new Transition (2814, 2815), // &gimel -> ℷ + new Transition (2818, 2819), // &GJcy -> Ѓ + new Transition (2822, 2823), // &gjcy -> ѓ + new Transition (2824, 2825), // &gl -> ≷ + new Transition (2826, 2827), // &gla -> ⪥ + new Transition (2828, 2829), // &glE -> ⪒ + new Transition (2830, 2831), // &glj -> ⪤ + new Transition (2834, 2835), // &gnap -> ⪊ + new Transition (2839, 2840), // &gnapprox -> ⪊ + new Transition (2841, 2842), // &gnE -> ≩ + new Transition (2843, 2844), // &gne -> ⪈ + new Transition (2845, 2846), // &gneq -> ⪈ + new Transition (2847, 2848), // &gneqq -> ≩ + new Transition (2851, 2852), // &gnsim -> ⋧ + new Transition (2855, 2856), // &Gopf -> 𝔾 + new Transition (2859, 2860), // &gopf -> 𝕘 + new Transition (2864, 2865), // &grave -> ` + new Transition (2876, 2877), // &GreaterEqual -> ≥ + new Transition (2881, 2882), // &GreaterEqualLess -> ⋛ + new Transition (2891, 2892), // &GreaterFullEqual -> ≧ + new Transition (2899, 2900), // &GreaterGreater -> ⪢ + new Transition (2904, 2905), // &GreaterLess -> ≷ + new Transition (2915, 2916), // &GreaterSlantEqual -> ⩾ + new Transition (2921, 2922), // &GreaterTilde -> ≳ + new Transition (2925, 2926), // &Gscr -> 𝒢 + new Transition (2929, 2930), // &gscr -> ℊ + new Transition (2932, 2933), // &gsim -> ≳ + new Transition (2934, 2935), // &gsime -> ⪎ + new Transition (2936, 2937), // &gsiml -> ⪐ + new Transition (2938, 2939), // > -> > + new Transition (2940, 2941), // &Gt -> ≫ + new Transition (2942, 2943), // > -> > + new Transition (2945, 2946), // >cc -> ⪧ + new Transition (2948, 2949), // >cir -> ⩺ + new Transition (2952, 2953), // >dot -> ⋗ + new Transition (2957, 2958), // >lPar -> ⦕ + new Transition (2963, 2964), // >quest -> ⩼ + new Transition (2971, 2972), // >rapprox -> ⪆ + new Transition (2974, 2975), // >rarr -> ⥸ + new Transition (2978, 2979), // >rdot -> ⋗ + new Transition (2985, 2986), // >reqless -> ⋛ + new Transition (2991, 2992), // >reqqless -> ⪌ + new Transition (2996, 2997), // >rless -> ≷ + new Transition (3000, 3001), // >rsim -> ≳ + new Transition (3009, 3010), // &gvertneqq -> ≩︀ + new Transition (3012, 3013), // &gvnE -> ≩︀ + new Transition (3018, 3019), // &Hacek -> ˇ + new Transition (3025, 3026), // &hairsp ->   + new Transition (3028, 3029), // &half -> ½ + new Transition (3033, 3034), // &hamilt -> ℋ + new Transition (3039, 3040), // &HARDcy -> Ъ + new Transition (3044, 3045), // &hardcy -> ъ + new Transition (3048, 3049), // &hArr -> ⇔ + new Transition (3050, 3051), // &harr -> ↔ + new Transition (3054, 3055), // &harrcir -> ⥈ + new Transition (3056, 3057), // &harrw -> ↭ + new Transition (3058, 3059), // &Hat -> ^ + new Transition (3062, 3063), // &hbar -> ℏ + new Transition (3067, 3068), // &Hcirc -> Ĥ + new Transition (3072, 3073), // &hcirc -> ĥ + new Transition (3078, 3079), // &hearts -> ♥ + new Transition (3082, 3083), // &heartsuit -> ♥ + new Transition (3087, 3088), // &hellip -> … + new Transition (3092, 3093), // &hercon -> ⊹ + new Transition (3095, 3096), // &Hfr -> ℌ + new Transition (3098, 3099), // &hfr -> 𝔥 + new Transition (3110, 3111), // &HilbertSpace -> ℋ + new Transition (3118, 3119), // &hksearow -> ⤥ + new Transition (3124, 3125), // &hkswarow -> ⤦ + new Transition (3129, 3130), // &hoarr -> ⇿ + new Transition (3134, 3135), // &homtht -> ∻ + new Transition (3146, 3147), // &hookleftarrow -> ↩ + new Transition (3157, 3158), // &hookrightarrow -> ↪ + new Transition (3161, 3162), // &Hopf -> ℍ + new Transition (3164, 3165), // &hopf -> 𝕙 + new Transition (3169, 3170), // &horbar -> ― + new Transition (3182, 3183), // &HorizontalLine -> ─ + new Transition (3186, 3187), // &Hscr -> ℋ + new Transition (3190, 3191), // &hscr -> 𝒽 + new Transition (3195, 3196), // &hslash -> ℏ + new Transition (3200, 3201), // &Hstrok -> Ħ + new Transition (3205, 3206), // &hstrok -> ħ + new Transition (3217, 3218), // &HumpDownHump -> ≎ + new Transition (3223, 3224), // &HumpEqual -> ≏ + new Transition (3229, 3230), // &hybull -> ⁃ + new Transition (3234, 3235), // &hyphen -> ‐ + new Transition (3241, 3242), // Í -> Í + new Transition (3248, 3249), // í -> í + new Transition (3250, 3251), // &ic -> ⁣ + new Transition (3255, 3256), // Î -> Î + new Transition (3259, 3260), // î -> î + new Transition (3261, 3262), // &Icy -> И + new Transition (3263, 3264), // &icy -> и + new Transition (3267, 3268), // &Idot -> İ + new Transition (3271, 3272), // &IEcy -> Е + new Transition (3275, 3276), // &iecy -> е + new Transition (3279, 3280), // ¡ -> ¡ + new Transition (3282, 3283), // &iff -> ⇔ + new Transition (3285, 3286), // &Ifr -> ℑ + new Transition (3287, 3288), // &ifr -> 𝔦 + new Transition (3293, 3294), // Ì -> Ì + new Transition (3299, 3300), // ì -> ì + new Transition (3301, 3302), // &ii -> ⅈ + new Transition (3306, 3307), // &iiiint -> ⨌ + new Transition (3309, 3310), // &iiint -> ∭ + new Transition (3314, 3315), // &iinfin -> ⧜ + new Transition (3318, 3319), // &iiota -> ℩ + new Transition (3323, 3324), // &IJlig -> IJ + new Transition (3328, 3329), // &ijlig -> ij + new Transition (3330, 3331), // &Im -> ℑ + new Transition (3334, 3335), // &Imacr -> Ī + new Transition (3339, 3340), // &imacr -> ī + new Transition (3342, 3343), // &image -> ℑ + new Transition (3350, 3351), // &ImaginaryI -> ⅈ + new Transition (3355, 3356), // &imagline -> ℐ + new Transition (3360, 3361), // &imagpart -> ℑ + new Transition (3363, 3364), // &imath -> ı + new Transition (3366, 3367), // &imof -> ⊷ + new Transition (3370, 3371), // &imped -> Ƶ + new Transition (3376, 3377), // &Implies -> ⇒ + new Transition (3378, 3379), // &in -> ∈ + new Transition (3383, 3384), // &incare -> ℅ + new Transition (3387, 3388), // &infin -> ∞ + new Transition (3391, 3392), // &infintie -> ⧝ + new Transition (3396, 3397), // &inodot -> ı + new Transition (3399, 3400), // &Int -> ∬ + new Transition (3401, 3402), // &int -> ∫ + new Transition (3405, 3406), // &intcal -> ⊺ + new Transition (3411, 3412), // &integers -> ℤ + new Transition (3417, 3418), // &Integral -> ∫ + new Transition (3422, 3423), // &intercal -> ⊺ + new Transition (3431, 3432), // &Intersection -> ⋂ + new Transition (3437, 3438), // &intlarhk -> ⨗ + new Transition (3442, 3443), // &intprod -> ⨼ + new Transition (3455, 3456), // &InvisibleComma -> ⁣ + new Transition (3461, 3462), // &InvisibleTimes -> ⁢ + new Transition (3465, 3466), // &IOcy -> Ё + new Transition (3469, 3470), // &iocy -> ё + new Transition (3474, 3475), // &Iogon -> Į + new Transition (3478, 3479), // &iogon -> į + new Transition (3481, 3482), // &Iopf -> 𝕀 + new Transition (3484, 3485), // &iopf -> 𝕚 + new Transition (3487, 3488), // &Iota -> Ι + new Transition (3490, 3491), // &iota -> ι + new Transition (3495, 3496), // &iprod -> ⨼ + new Transition (3501, 3502), // ¿ -> ¿ + new Transition (3505, 3506), // &Iscr -> ℐ + new Transition (3509, 3510), // &iscr -> 𝒾 + new Transition (3512, 3513), // &isin -> ∈ + new Transition (3516, 3517), // &isindot -> ⋵ + new Transition (3518, 3519), // &isinE -> ⋹ + new Transition (3520, 3521), // &isins -> ⋴ + new Transition (3522, 3523), // &isinsv -> ⋳ + new Transition (3524, 3525), // &isinv -> ∈ + new Transition (3526, 3527), // &it -> ⁢ + new Transition (3532, 3533), // &Itilde -> Ĩ + new Transition (3537, 3538), // &itilde -> ĩ + new Transition (3542, 3543), // &Iukcy -> І + new Transition (3547, 3548), // &iukcy -> і + new Transition (3550, 3551), // Ï -> Ï + new Transition (3553, 3554), // ï -> ï + new Transition (3559, 3560), // &Jcirc -> Ĵ + new Transition (3565, 3566), // &jcirc -> ĵ + new Transition (3567, 3568), // &Jcy -> Й + new Transition (3569, 3570), // &jcy -> й + new Transition (3572, 3573), // &Jfr -> 𝔍 + new Transition (3575, 3576), // &jfr -> 𝔧 + new Transition (3580, 3581), // &jmath -> ȷ + new Transition (3584, 3585), // &Jopf -> 𝕁 + new Transition (3588, 3589), // &jopf -> 𝕛 + new Transition (3592, 3593), // &Jscr -> 𝒥 + new Transition (3596, 3597), // &jscr -> 𝒿 + new Transition (3601, 3602), // &Jsercy -> Ј + new Transition (3606, 3607), // &jsercy -> ј + new Transition (3611, 3612), // &Jukcy -> Є + new Transition (3616, 3617), // &jukcy -> є + new Transition (3622, 3623), // &Kappa -> Κ + new Transition (3628, 3629), // &kappa -> κ + new Transition (3630, 3631), // &kappav -> ϰ + new Transition (3636, 3637), // &Kcedil -> Ķ + new Transition (3642, 3643), // &kcedil -> ķ + new Transition (3644, 3645), // &Kcy -> К + new Transition (3646, 3647), // &kcy -> к + new Transition (3649, 3650), // &Kfr -> 𝔎 + new Transition (3652, 3653), // &kfr -> 𝔨 + new Transition (3658, 3659), // &kgreen -> ĸ + new Transition (3662, 3663), // &KHcy -> Х + new Transition (3666, 3667), // &khcy -> х + new Transition (3670, 3671), // &KJcy -> Ќ + new Transition (3674, 3675), // &kjcy -> ќ + new Transition (3678, 3679), // &Kopf -> 𝕂 + new Transition (3682, 3683), // &kopf -> 𝕜 + new Transition (3686, 3687), // &Kscr -> 𝒦 + new Transition (3690, 3691), // &kscr -> 𝓀 + new Transition (3696, 3697), // &lAarr -> ⇚ + new Transition (3703, 3704), // &Lacute -> Ĺ + new Transition (3709, 3710), // &lacute -> ĺ + new Transition (3716, 3717), // &laemptyv -> ⦴ + new Transition (3721, 3722), // &lagran -> ℒ + new Transition (3726, 3727), // &Lambda -> Λ + new Transition (3731, 3732), // &lambda -> λ + new Transition (3734, 3735), // &Lang -> ⟪ + new Transition (3737, 3738), // &lang -> ⟨ + new Transition (3739, 3740), // &langd -> ⦑ + new Transition (3742, 3743), // &langle -> ⟨ + new Transition (3744, 3745), // &lap -> ⪅ + new Transition (3753, 3754), // &Laplacetrf -> ℒ + new Transition (3757, 3758), // « -> « + new Transition (3760, 3761), // &Larr -> ↞ + new Transition (3763, 3764), // &lArr -> ⇐ + new Transition (3766, 3767), // &larr -> ← + new Transition (3768, 3769), // &larrb -> ⇤ + new Transition (3771, 3772), // &larrbfs -> ⤟ + new Transition (3774, 3775), // &larrfs -> ⤝ + new Transition (3777, 3778), // &larrhk -> ↩ + new Transition (3780, 3781), // &larrlp -> ↫ + new Transition (3783, 3784), // &larrpl -> ⤹ + new Transition (3787, 3788), // &larrsim -> ⥳ + new Transition (3790, 3791), // &larrtl -> ↢ + new Transition (3792, 3793), // &lat -> ⪫ + new Transition (3797, 3798), // &lAtail -> ⤛ + new Transition (3801, 3802), // &latail -> ⤙ + new Transition (3803, 3804), // &late -> ⪭ + new Transition (3805, 3806), // &lates -> ⪭︀ + new Transition (3810, 3811), // &lBarr -> ⤎ + new Transition (3815, 3816), // &lbarr -> ⤌ + new Transition (3819, 3820), // &lbbrk -> ❲ + new Transition (3824, 3825), // &lbrace -> { + new Transition (3826, 3827), // &lbrack -> [ + new Transition (3829, 3830), // &lbrke -> ⦋ + new Transition (3833, 3834), // &lbrksld -> ⦏ + new Transition (3835, 3836), // &lbrkslu -> ⦍ + new Transition (3841, 3842), // &Lcaron -> Ľ + new Transition (3847, 3848), // &lcaron -> ľ + new Transition (3852, 3853), // &Lcedil -> Ļ + new Transition (3857, 3858), // &lcedil -> ļ + new Transition (3860, 3861), // &lceil -> ⌈ + new Transition (3863, 3864), // &lcub -> { + new Transition (3865, 3866), // &Lcy -> Л + new Transition (3867, 3868), // &lcy -> л + new Transition (3871, 3872), // &ldca -> ⤶ + new Transition (3875, 3876), // &ldquo -> “ + new Transition (3877, 3878), // &ldquor -> „ + new Transition (3883, 3884), // &ldrdhar -> ⥧ + new Transition (3889, 3890), // &ldrushar -> ⥋ + new Transition (3892, 3893), // &ldsh -> ↲ + new Transition (3894, 3895), // &lE -> ≦ + new Transition (3896, 3897), // &le -> ≤ + new Transition (3912, 3913), // &LeftAngleBracket -> ⟨ + new Transition (3917, 3918), // &LeftArrow -> ← + new Transition (3923, 3924), // &Leftarrow -> ⇐ + new Transition (3931, 3932), // &leftarrow -> ← + new Transition (3935, 3936), // &LeftArrowBar -> ⇤ + new Transition (3946, 3947), // &LeftArrowRightArrow -> ⇆ + new Transition (3951, 3952), // &leftarrowtail -> ↢ + new Transition (3959, 3960), // &LeftCeiling -> ⌈ + new Transition (3973, 3974), // &LeftDoubleBracket -> ⟦ + new Transition (3985, 3986), // &LeftDownTeeVector -> ⥡ + new Transition (3992, 3993), // &LeftDownVector -> ⇃ + new Transition (3996, 3997), // &LeftDownVectorBar -> ⥙ + new Transition (4002, 4003), // &LeftFloor -> ⌊ + new Transition (4014, 4015), // &leftharpoondown -> ↽ + new Transition (4017, 4018), // &leftharpoonup -> ↼ + new Transition (4028, 4029), // &leftleftarrows -> ⇇ + new Transition (4039, 4040), // &LeftRightArrow -> ↔ + new Transition (4050, 4051), // &Leftrightarrow -> ⇔ + new Transition (4061, 4062), // &leftrightarrow -> ↔ + new Transition (4063, 4064), // &leftrightarrows -> ⇆ + new Transition (4072, 4073), // &leftrightharpoons -> ⇋ + new Transition (4083, 4084), // &leftrightsquigarrow -> ↭ + new Transition (4090, 4091), // &LeftRightVector -> ⥎ + new Transition (4094, 4095), // &LeftTee -> ⊣ + new Transition (4100, 4101), // &LeftTeeArrow -> ↤ + new Transition (4107, 4108), // &LeftTeeVector -> ⥚ + new Transition (4118, 4119), // &leftthreetimes -> ⋋ + new Transition (4126, 4127), // &LeftTriangle -> ⊲ + new Transition (4130, 4131), // &LeftTriangleBar -> ⧏ + new Transition (4136, 4137), // &LeftTriangleEqual -> ⊴ + new Transition (4149, 4150), // &LeftUpDownVector -> ⥑ + new Transition (4159, 4160), // &LeftUpTeeVector -> ⥠ + new Transition (4166, 4167), // &LeftUpVector -> ↿ + new Transition (4170, 4171), // &LeftUpVectorBar -> ⥘ + new Transition (4177, 4178), // &LeftVector -> ↼ + new Transition (4181, 4182), // &LeftVectorBar -> ⥒ + new Transition (4183, 4184), // &lEg -> ⪋ + new Transition (4185, 4186), // &leg -> ⋚ + new Transition (4187, 4188), // &leq -> ≤ + new Transition (4189, 4190), // &leqq -> ≦ + new Transition (4195, 4196), // &leqslant -> ⩽ + new Transition (4197, 4198), // &les -> ⩽ + new Transition (4200, 4201), // &lescc -> ⪨ + new Transition (4204, 4205), // &lesdot -> ⩿ + new Transition (4206, 4207), // &lesdoto -> ⪁ + new Transition (4208, 4209), // &lesdotor -> ⪃ + new Transition (4210, 4211), // &lesg -> ⋚︀ + new Transition (4213, 4214), // &lesges -> ⪓ + new Transition (4221, 4222), // &lessapprox -> ⪅ + new Transition (4225, 4226), // &lessdot -> ⋖ + new Transition (4231, 4232), // &lesseqgtr -> ⋚ + new Transition (4236, 4237), // &lesseqqgtr -> ⪋ + new Transition (4251, 4252), // &LessEqualGreater -> ⋚ + new Transition (4261, 4262), // &LessFullEqual -> ≦ + new Transition (4269, 4270), // &LessGreater -> ≶ + new Transition (4273, 4274), // &lessgtr -> ≶ + new Transition (4278, 4279), // &LessLess -> ⪡ + new Transition (4282, 4283), // &lesssim -> ≲ + new Transition (4293, 4294), // &LessSlantEqual -> ⩽ + new Transition (4299, 4300), // &LessTilde -> ≲ + new Transition (4305, 4306), // &lfisht -> ⥼ + new Transition (4310, 4311), // &lfloor -> ⌊ + new Transition (4313, 4314), // &Lfr -> 𝔏 + new Transition (4315, 4316), // &lfr -> 𝔩 + new Transition (4317, 4318), // &lg -> ≶ + new Transition (4319, 4320), // &lgE -> ⪑ + new Transition (4323, 4324), // &lHar -> ⥢ + new Transition (4328, 4329), // &lhard -> ↽ + new Transition (4330, 4331), // &lharu -> ↼ + new Transition (4332, 4333), // &lharul -> ⥪ + new Transition (4336, 4337), // &lhblk -> ▄ + new Transition (4340, 4341), // &LJcy -> Љ + new Transition (4344, 4345), // &ljcy -> љ + new Transition (4346, 4347), // &Ll -> ⋘ + new Transition (4348, 4349), // &ll -> ≪ + new Transition (4352, 4353), // &llarr -> ⇇ + new Transition (4359, 4360), // &llcorner -> ⌞ + new Transition (4368, 4369), // &Lleftarrow -> ⇚ + new Transition (4373, 4374), // &llhard -> ⥫ + new Transition (4377, 4378), // &lltri -> ◺ + new Transition (4383, 4384), // &Lmidot -> Ŀ + new Transition (4389, 4390), // &lmidot -> ŀ + new Transition (4394, 4395), // &lmoust -> ⎰ + new Transition (4399, 4400), // &lmoustache -> ⎰ + new Transition (4403, 4404), // &lnap -> ⪉ + new Transition (4408, 4409), // &lnapprox -> ⪉ + new Transition (4410, 4411), // &lnE -> ≨ + new Transition (4412, 4413), // &lne -> ⪇ + new Transition (4414, 4415), // &lneq -> ⪇ + new Transition (4416, 4417), // &lneqq -> ≨ + new Transition (4420, 4421), // &lnsim -> ⋦ + new Transition (4425, 4426), // &loang -> ⟬ + new Transition (4428, 4429), // &loarr -> ⇽ + new Transition (4432, 4433), // &lobrk -> ⟦ + new Transition (4445, 4446), // &LongLeftArrow -> ⟵ + new Transition (4455, 4456), // &Longleftarrow -> ⟸ + new Transition (4467, 4468), // &longleftarrow -> ⟵ + new Transition (4478, 4479), // &LongLeftRightArrow -> ⟷ + new Transition (4489, 4490), // &Longleftrightarrow -> ⟺ + new Transition (4500, 4501), // &longleftrightarrow -> ⟷ + new Transition (4507, 4508), // &longmapsto -> ⟼ + new Transition (4518, 4519), // &LongRightArrow -> ⟶ + new Transition (4529, 4530), // &Longrightarrow -> ⟹ + new Transition (4540, 4541), // &longrightarrow -> ⟶ + new Transition (4552, 4553), // &looparrowleft -> ↫ + new Transition (4558, 4559), // &looparrowright -> ↬ + new Transition (4562, 4563), // &lopar -> ⦅ + new Transition (4565, 4566), // &Lopf -> 𝕃 + new Transition (4567, 4568), // &lopf -> 𝕝 + new Transition (4571, 4572), // &loplus -> ⨭ + new Transition (4577, 4578), // &lotimes -> ⨴ + new Transition (4582, 4583), // &lowast -> ∗ + new Transition (4586, 4587), // &lowbar -> _ + new Transition (4599, 4600), // &LowerLeftArrow -> ↙ + new Transition (4610, 4611), // &LowerRightArrow -> ↘ + new Transition (4612, 4613), // &loz -> ◊ + new Transition (4617, 4618), // &lozenge -> ◊ + new Transition (4619, 4620), // &lozf -> ⧫ + new Transition (4623, 4624), // &lpar -> ( + new Transition (4626, 4627), // &lparlt -> ⦓ + new Transition (4631, 4632), // &lrarr -> ⇆ + new Transition (4638, 4639), // &lrcorner -> ⌟ + new Transition (4642, 4643), // &lrhar -> ⇋ + new Transition (4644, 4645), // &lrhard -> ⥭ + new Transition (4646, 4647), // &lrm -> ‎ + new Transition (4650, 4651), // &lrtri -> ⊿ + new Transition (4656, 4657), // &lsaquo -> ‹ + new Transition (4660, 4661), // &Lscr -> ℒ + new Transition (4663, 4664), // &lscr -> 𝓁 + new Transition (4665, 4666), // &Lsh -> ↰ + new Transition (4667, 4668), // &lsh -> ↰ + new Transition (4670, 4671), // &lsim -> ≲ + new Transition (4672, 4673), // &lsime -> ⪍ + new Transition (4674, 4675), // &lsimg -> ⪏ + new Transition (4677, 4678), // &lsqb -> [ + new Transition (4680, 4681), // &lsquo -> ‘ + new Transition (4682, 4683), // &lsquor -> ‚ + new Transition (4687, 4688), // &Lstrok -> Ł + new Transition (4692, 4693), // &lstrok -> ł + new Transition (4694, 4695), // < -> < + new Transition (4696, 4697), // &Lt -> ≪ + new Transition (4698, 4699), // < -> < + new Transition (4701, 4702), // <cc -> ⪦ + new Transition (4704, 4705), // <cir -> ⩹ + new Transition (4708, 4709), // <dot -> ⋖ + new Transition (4713, 4714), // <hree -> ⋋ + new Transition (4718, 4719), // <imes -> ⋉ + new Transition (4723, 4724), // <larr -> ⥶ + new Transition (4729, 4730), // <quest -> ⩻ + new Transition (4732, 4733), // <ri -> ◃ + new Transition (4734, 4735), // <rie -> ⊴ + new Transition (4736, 4737), // <rif -> ◂ + new Transition (4740, 4741), // <rPar -> ⦖ + new Transition (4748, 4749), // &lurdshar -> ⥊ + new Transition (4753, 4754), // &luruhar -> ⥦ + new Transition (4762, 4763), // &lvertneqq -> ≨︀ + new Transition (4765, 4766), // &lvnE -> ≨︀ + new Transition (4770, 4771), // ¯ -> ¯ + new Transition (4773, 4774), // &male -> ♂ + new Transition (4775, 4776), // &malt -> ✠ + new Transition (4779, 4780), // &maltese -> ✠ + new Transition (4783, 4784), // &Map -> ⤅ + new Transition (4785, 4786), // &map -> ↦ + new Transition (4789, 4790), // &mapsto -> ↦ + new Transition (4794, 4795), // &mapstodown -> ↧ + new Transition (4799, 4800), // &mapstoleft -> ↤ + new Transition (4802, 4803), // &mapstoup -> ↥ + new Transition (4807, 4808), // &marker -> ▮ + new Transition (4813, 4814), // &mcomma -> ⨩ + new Transition (4816, 4817), // &Mcy -> М + new Transition (4818, 4819), // &mcy -> м + new Transition (4823, 4824), // &mdash -> — + new Transition (4828, 4829), // &mDDot -> ∺ + new Transition (4841, 4842), // &measuredangle -> ∡ + new Transition (4852, 4853), // &MediumSpace ->   + new Transition (4860, 4861), // &Mellintrf -> ℳ + new Transition (4863, 4864), // &Mfr -> 𝔐 + new Transition (4866, 4867), // &mfr -> 𝔪 + new Transition (4869, 4870), // &mho -> ℧ + new Transition (4874, 4875), // µ -> µ + new Transition (4876, 4877), // &mid -> ∣ + new Transition (4880, 4881), // &midast -> * + new Transition (4884, 4885), // &midcir -> ⫰ + new Transition (4888, 4889), // · -> · + new Transition (4892, 4893), // &minus -> − + new Transition (4894, 4895), // &minusb -> ⊟ + new Transition (4896, 4897), // &minusd -> ∸ + new Transition (4898, 4899), // &minusdu -> ⨪ + new Transition (4907, 4908), // &MinusPlus -> ∓ + new Transition (4911, 4912), // &mlcp -> ⫛ + new Transition (4914, 4915), // &mldr -> … + new Transition (4920, 4921), // &mnplus -> ∓ + new Transition (4926, 4927), // &models -> ⊧ + new Transition (4930, 4931), // &Mopf -> 𝕄 + new Transition (4933, 4934), // &mopf -> 𝕞 + new Transition (4935, 4936), // &mp -> ∓ + new Transition (4939, 4940), // &Mscr -> ℳ + new Transition (4943, 4944), // &mscr -> 𝓂 + new Transition (4948, 4949), // &mstpos -> ∾ + new Transition (4950, 4951), // &Mu -> Μ + new Transition (4952, 4953), // &mu -> μ + new Transition (4959, 4960), // &multimap -> ⊸ + new Transition (4963, 4964), // &mumap -> ⊸ + new Transition (4969, 4970), // &nabla -> ∇ + new Transition (4976, 4977), // &Nacute -> Ń + new Transition (4981, 4982), // &nacute -> ń + new Transition (4984, 4985), // &nang -> ∠⃒ + new Transition (4986, 4987), // &nap -> ≉ + new Transition (4988, 4989), // &napE -> ⩰̸ + new Transition (4991, 4992), // &napid -> ≋̸ + new Transition (4994, 4995), // &napos -> ʼn + new Transition (4999, 5000), // &napprox -> ≉ + new Transition (5003, 5004), // &natur -> ♮ + new Transition (5006, 5007), // &natural -> ♮ + new Transition (5008, 5009), // &naturals -> ℕ + new Transition (5012, 5013), //   ->   + new Transition (5016, 5017), // &nbump -> ≎̸ + new Transition (5018, 5019), // &nbumpe -> ≏̸ + new Transition (5022, 5023), // &ncap -> ⩃ + new Transition (5028, 5029), // &Ncaron -> Ň + new Transition (5032, 5033), // &ncaron -> ň + new Transition (5037, 5038), // &Ncedil -> Ņ + new Transition (5042, 5043), // &ncedil -> ņ + new Transition (5046, 5047), // &ncong -> ≇ + new Transition (5050, 5051), // &ncongdot -> ⩭̸ + new Transition (5053, 5054), // &ncup -> ⩂ + new Transition (5055, 5056), // &Ncy -> Н + new Transition (5057, 5058), // &ncy -> н + new Transition (5062, 5063), // &ndash -> – + new Transition (5064, 5065), // &ne -> ≠ + new Transition (5069, 5070), // &nearhk -> ⤤ + new Transition (5073, 5074), // &neArr -> ⇗ + new Transition (5075, 5076), // &nearr -> ↗ + new Transition (5078, 5079), // &nearrow -> ↗ + new Transition (5082, 5083), // &nedot -> ≐̸ + new Transition (5101, 5102), // &NegativeMediumSpace -> ​ + new Transition (5112, 5113), // &NegativeThickSpace -> ​ + new Transition (5119, 5120), // &NegativeThinSpace -> ​ + new Transition (5133, 5134), // &NegativeVeryThinSpace -> ​ + new Transition (5138, 5139), // &nequiv -> ≢ + new Transition (5143, 5144), // &nesear -> ⤨ + new Transition (5146, 5147), // &nesim -> ≂̸ + new Transition (5165, 5166), // &NestedGreaterGreater -> ≫ + new Transition (5174, 5175), // &NestedLessLess -> ≪ + new Transition (5180, 5181), // &NewLine -> + new Transition (5185, 5186), // &nexist -> ∄ + new Transition (5187, 5188), // &nexists -> ∄ + new Transition (5190, 5191), // &Nfr -> 𝔑 + new Transition (5193, 5194), // &nfr -> 𝔫 + new Transition (5196, 5197), // &ngE -> ≧̸ + new Transition (5198, 5199), // &nge -> ≱ + new Transition (5200, 5201), // &ngeq -> ≱ + new Transition (5202, 5203), // &ngeqq -> ≧̸ + new Transition (5208, 5209), // &ngeqslant -> ⩾̸ + new Transition (5210, 5211), // &nges -> ⩾̸ + new Transition (5213, 5214), // &nGg -> ⋙̸ + new Transition (5217, 5218), // &ngsim -> ≵ + new Transition (5219, 5220), // &nGt -> ≫⃒ + new Transition (5221, 5222), // &ngt -> ≯ + new Transition (5223, 5224), // &ngtr -> ≯ + new Transition (5225, 5226), // &nGtv -> ≫̸ + new Transition (5230, 5231), // &nhArr -> ⇎ + new Transition (5234, 5235), // &nharr -> ↮ + new Transition (5238, 5239), // &nhpar -> ⫲ + new Transition (5240, 5241), // &ni -> ∋ + new Transition (5242, 5243), // &nis -> ⋼ + new Transition (5244, 5245), // &nisd -> ⋺ + new Transition (5246, 5247), // &niv -> ∋ + new Transition (5250, 5251), // &NJcy -> Њ + new Transition (5254, 5255), // &njcy -> њ + new Transition (5259, 5260), // &nlArr -> ⇍ + new Transition (5263, 5264), // &nlarr -> ↚ + new Transition (5266, 5267), // &nldr -> ‥ + new Transition (5268, 5269), // &nlE -> ≦̸ + new Transition (5270, 5271), // &nle -> ≰ + new Transition (5280, 5281), // &nLeftarrow -> ⇍ + new Transition (5288, 5289), // &nleftarrow -> ↚ + new Transition (5299, 5300), // &nLeftrightarrow -> ⇎ + new Transition (5310, 5311), // &nleftrightarrow -> ↮ + new Transition (5312, 5313), // &nleq -> ≰ + new Transition (5314, 5315), // &nleqq -> ≦̸ + new Transition (5320, 5321), // &nleqslant -> ⩽̸ + new Transition (5322, 5323), // &nles -> ⩽̸ + new Transition (5324, 5325), // &nless -> ≮ + new Transition (5326, 5327), // &nLl -> ⋘̸ + new Transition (5330, 5331), // &nlsim -> ≴ + new Transition (5332, 5333), // &nLt -> ≪⃒ + new Transition (5334, 5335), // &nlt -> ≮ + new Transition (5337, 5338), // &nltri -> ⋪ + new Transition (5339, 5340), // &nltrie -> ⋬ + new Transition (5341, 5342), // &nLtv -> ≪̸ + new Transition (5345, 5346), // &nmid -> ∤ + new Transition (5352, 5353), // &NoBreak -> ⁠ + new Transition (5367, 5368), // &NonBreakingSpace ->   + new Transition (5370, 5371), // &Nopf -> ℕ + new Transition (5374, 5375), // &nopf -> 𝕟 + new Transition (5376, 5377), // &Not -> ⫬ + new Transition (5378, 5379), // ¬ -> ¬ + new Transition (5388, 5389), // &NotCongruent -> ≢ + new Transition (5394, 5395), // &NotCupCap -> ≭ + new Transition (5412, 5413), // &NotDoubleVerticalBar -> ∦ + new Transition (5420, 5421), // &NotElement -> ∉ + new Transition (5425, 5426), // &NotEqual -> ≠ + new Transition (5431, 5432), // &NotEqualTilde -> ≂̸ + new Transition (5437, 5438), // &NotExists -> ∄ + new Transition (5445, 5446), // &NotGreater -> ≯ + new Transition (5451, 5452), // &NotGreaterEqual -> ≱ + new Transition (5461, 5462), // &NotGreaterFullEqual -> ≧̸ + new Transition (5469, 5470), // &NotGreaterGreater -> ≫̸ + new Transition (5474, 5475), // &NotGreaterLess -> ≹ + new Transition (5485, 5486), // &NotGreaterSlantEqual -> ⩾̸ + new Transition (5491, 5492), // &NotGreaterTilde -> ≵ + new Transition (5504, 5505), // &NotHumpDownHump -> ≎̸ + new Transition (5510, 5511), // &NotHumpEqual -> ≏̸ + new Transition (5513, 5514), // ¬in -> ∉ + new Transition (5517, 5518), // ¬indot -> ⋵̸ + new Transition (5519, 5520), // ¬inE -> ⋹̸ + new Transition (5522, 5523), // ¬inva -> ∉ + new Transition (5524, 5525), // ¬invb -> ⋷ + new Transition (5526, 5527), // ¬invc -> ⋶ + new Transition (5539, 5540), // &NotLeftTriangle -> ⋪ + new Transition (5543, 5544), // &NotLeftTriangleBar -> ⧏̸ + new Transition (5549, 5550), // &NotLeftTriangleEqual -> ⋬ + new Transition (5552, 5553), // &NotLess -> ≮ + new Transition (5558, 5559), // &NotLessEqual -> ≰ + new Transition (5566, 5567), // &NotLessGreater -> ≸ + new Transition (5571, 5572), // &NotLessLess -> ≪̸ + new Transition (5582, 5583), // &NotLessSlantEqual -> ⩽̸ + new Transition (5588, 5589), // &NotLessTilde -> ≴ + new Transition (5609, 5610), // &NotNestedGreaterGreater -> ⪢̸ + new Transition (5618, 5619), // &NotNestedLessLess -> ⪡̸ + new Transition (5621, 5622), // ¬ni -> ∌ + new Transition (5624, 5625), // ¬niva -> ∌ + new Transition (5626, 5627), // ¬nivb -> ⋾ + new Transition (5628, 5629), // ¬nivc -> ⋽ + new Transition (5637, 5638), // &NotPrecedes -> ⊀ + new Transition (5643, 5644), // &NotPrecedesEqual -> ⪯̸ + new Transition (5654, 5655), // &NotPrecedesSlantEqual -> ⋠ + new Transition (5669, 5670), // &NotReverseElement -> ∌ + new Transition (5682, 5683), // &NotRightTriangle -> ⋫ + new Transition (5686, 5687), // &NotRightTriangleBar -> ⧐̸ + new Transition (5692, 5693), // &NotRightTriangleEqual -> ⋭ + new Transition (5705, 5706), // &NotSquareSubset -> ⊏̸ + new Transition (5711, 5712), // &NotSquareSubsetEqual -> ⋢ + new Transition (5718, 5719), // &NotSquareSuperset -> ⊐̸ + new Transition (5724, 5725), // &NotSquareSupersetEqual -> ⋣ + new Transition (5730, 5731), // &NotSubset -> ⊂⃒ + new Transition (5736, 5737), // &NotSubsetEqual -> ⊈ + new Transition (5743, 5744), // &NotSucceeds -> ⊁ + new Transition (5749, 5750), // &NotSucceedsEqual -> ⪰̸ + new Transition (5760, 5761), // &NotSucceedsSlantEqual -> ⋡ + new Transition (5766, 5767), // &NotSucceedsTilde -> ≿̸ + new Transition (5773, 5774), // &NotSuperset -> ⊃⃒ + new Transition (5779, 5780), // &NotSupersetEqual -> ⊉ + new Transition (5785, 5786), // &NotTilde -> ≁ + new Transition (5791, 5792), // &NotTildeEqual -> ≄ + new Transition (5801, 5802), // &NotTildeFullEqual -> ≇ + new Transition (5807, 5808), // &NotTildeTilde -> ≉ + new Transition (5819, 5820), // &NotVerticalBar -> ∤ + new Transition (5823, 5824), // &npar -> ∦ + new Transition (5829, 5830), // &nparallel -> ∦ + new Transition (5832, 5833), // &nparsl -> ⫽⃥ + new Transition (5834, 5835), // &npart -> ∂̸ + new Transition (5840, 5841), // &npolint -> ⨔ + new Transition (5842, 5843), // &npr -> ⊀ + new Transition (5846, 5847), // &nprcue -> ⋠ + new Transition (5848, 5849), // &npre -> ⪯̸ + new Transition (5850, 5851), // &nprec -> ⊀ + new Transition (5853, 5854), // &npreceq -> ⪯̸ + new Transition (5858, 5859), // &nrArr -> ⇏ + new Transition (5862, 5863), // &nrarr -> ↛ + new Transition (5864, 5865), // &nrarrc -> ⤳̸ + new Transition (5866, 5867), // &nrarrw -> ↝̸ + new Transition (5877, 5878), // &nRightarrow -> ⇏ + new Transition (5887, 5888), // &nrightarrow -> ↛ + new Transition (5891, 5892), // &nrtri -> ⋫ + new Transition (5893, 5894), // &nrtrie -> ⋭ + new Transition (5896, 5897), // &nsc -> ⊁ + new Transition (5900, 5901), // &nsccue -> ⋡ + new Transition (5902, 5903), // &nsce -> ⪰̸ + new Transition (5906, 5907), // &Nscr -> 𝒩 + new Transition (5908, 5909), // &nscr -> 𝓃 + new Transition (5916, 5917), // &nshortmid -> ∤ + new Transition (5925, 5926), // &nshortparallel -> ∦ + new Transition (5928, 5929), // &nsim -> ≁ + new Transition (5930, 5931), // &nsime -> ≄ + new Transition (5932, 5933), // &nsimeq -> ≄ + new Transition (5936, 5937), // &nsmid -> ∤ + new Transition (5940, 5941), // &nspar -> ∦ + new Transition (5946, 5947), // &nsqsube -> ⋢ + new Transition (5949, 5950), // &nsqsupe -> ⋣ + new Transition (5952, 5953), // &nsub -> ⊄ + new Transition (5954, 5955), // &nsubE -> ⫅̸ + new Transition (5956, 5957), // &nsube -> ⊈ + new Transition (5960, 5961), // &nsubset -> ⊂⃒ + new Transition (5963, 5964), // &nsubseteq -> ⊈ + new Transition (5965, 5966), // &nsubseteqq -> ⫅̸ + new Transition (5968, 5969), // &nsucc -> ⊁ + new Transition (5971, 5972), // &nsucceq -> ⪰̸ + new Transition (5973, 5974), // &nsup -> ⊅ + new Transition (5975, 5976), // &nsupE -> ⫆̸ + new Transition (5977, 5978), // &nsupe -> ⊉ + new Transition (5981, 5982), // &nsupset -> ⊃⃒ + new Transition (5984, 5985), // &nsupseteq -> ⊉ + new Transition (5986, 5987), // &nsupseteqq -> ⫆̸ + new Transition (5990, 5991), // &ntgl -> ≹ + new Transition (5996, 5997), // Ñ -> Ñ + new Transition (6001, 6002), // ñ -> ñ + new Transition (6004, 6005), // &ntlg -> ≸ + new Transition (6016, 6017), // &ntriangleleft -> ⋪ + new Transition (6019, 6020), // &ntrianglelefteq -> ⋬ + new Transition (6025, 6026), // &ntriangleright -> ⋫ + new Transition (6028, 6029), // &ntrianglerighteq -> ⋭ + new Transition (6030, 6031), // &Nu -> Ν + new Transition (6032, 6033), // &nu -> ν + new Transition (6034, 6035), // &num -> # + new Transition (6038, 6039), // &numero -> № + new Transition (6041, 6042), // &numsp ->   + new Transition (6045, 6046), // &nvap -> ≍⃒ + new Transition (6051, 6052), // &nVDash -> ⊯ + new Transition (6056, 6057), // &nVdash -> ⊮ + new Transition (6061, 6062), // &nvDash -> ⊭ + new Transition (6066, 6067), // &nvdash -> ⊬ + new Transition (6069, 6070), // &nvge -> ≥⃒ + new Transition (6071, 6072), // &nvgt -> >⃒ + new Transition (6076, 6077), // &nvHarr -> ⤄ + new Transition (6082, 6083), // &nvinfin -> ⧞ + new Transition (6087, 6088), // &nvlArr -> ⤂ + new Transition (6089, 6090), // &nvle -> ≤⃒ + new Transition (6091, 6092), // &nvlt -> <⃒ + new Transition (6095, 6096), // &nvltrie -> ⊴⃒ + new Transition (6100, 6101), // &nvrArr -> ⤃ + new Transition (6105, 6106), // &nvrtrie -> ⊵⃒ + new Transition (6109, 6110), // &nvsim -> ∼⃒ + new Transition (6115, 6116), // &nwarhk -> ⤣ + new Transition (6119, 6120), // &nwArr -> ⇖ + new Transition (6121, 6122), // &nwarr -> ↖ + new Transition (6124, 6125), // &nwarrow -> ↖ + new Transition (6129, 6130), // &nwnear -> ⤧ + new Transition (6136, 6137), // Ó -> Ó + new Transition (6143, 6144), // ó -> ó + new Transition (6146, 6147), // &oast -> ⊛ + new Transition (6150, 6151), // &ocir -> ⊚ + new Transition (6155, 6156), // Ô -> Ô + new Transition (6157, 6158), // ô -> ô + new Transition (6159, 6160), // &Ocy -> О + new Transition (6161, 6162), // &ocy -> о + new Transition (6166, 6167), // &odash -> ⊝ + new Transition (6172, 6173), // &Odblac -> Ő + new Transition (6177, 6178), // &odblac -> ő + new Transition (6180, 6181), // &odiv -> ⨸ + new Transition (6183, 6184), // &odot -> ⊙ + new Transition (6188, 6189), // &odsold -> ⦼ + new Transition (6193, 6194), // &OElig -> Œ + new Transition (6198, 6199), // &oelig -> œ + new Transition (6203, 6204), // &ofcir -> ⦿ + new Transition (6206, 6207), // &Ofr -> 𝔒 + new Transition (6208, 6209), // &ofr -> 𝔬 + new Transition (6212, 6213), // &ogon -> ˛ + new Transition (6218, 6219), // Ò -> Ò + new Transition (6223, 6224), // ò -> ò + new Transition (6225, 6226), // &ogt -> ⧁ + new Transition (6230, 6231), // &ohbar -> ⦵ + new Transition (6232, 6233), // &ohm -> Ω + new Transition (6236, 6237), // &oint -> ∮ + new Transition (6241, 6242), // &olarr -> ↺ + new Transition (6245, 6246), // &olcir -> ⦾ + new Transition (6250, 6251), // &olcross -> ⦻ + new Transition (6254, 6255), // &oline -> ‾ + new Transition (6256, 6257), // &olt -> ⧀ + new Transition (6261, 6262), // &Omacr -> Ō + new Transition (6266, 6267), // &omacr -> ō + new Transition (6270, 6271), // &Omega -> Ω + new Transition (6274, 6275), // &omega -> ω + new Transition (6280, 6281), // &Omicron -> Ο + new Transition (6286, 6287), // &omicron -> ο + new Transition (6288, 6289), // &omid -> ⦶ + new Transition (6292, 6293), // &ominus -> ⊖ + new Transition (6296, 6297), // &Oopf -> 𝕆 + new Transition (6300, 6301), // &oopf -> 𝕠 + new Transition (6304, 6305), // &opar -> ⦷ + new Transition (6324, 6325), // &OpenCurlyDoubleQuote -> “ + new Transition (6330, 6331), // &OpenCurlyQuote -> ‘ + new Transition (6334, 6335), // &operp -> ⦹ + new Transition (6338, 6339), // &oplus -> ⊕ + new Transition (6340, 6341), // &Or -> ⩔ + new Transition (6342, 6343), // &or -> ∨ + new Transition (6346, 6347), // &orarr -> ↻ + new Transition (6348, 6349), // &ord -> ⩝ + new Transition (6351, 6352), // &order -> ℴ + new Transition (6354, 6355), // &orderof -> ℴ + new Transition (6356, 6357), // ª -> ª + new Transition (6358, 6359), // º -> º + new Transition (6363, 6364), // &origof -> ⊶ + new Transition (6366, 6367), // &oror -> ⩖ + new Transition (6372, 6373), // &orslope -> ⩗ + new Transition (6374, 6375), // &orv -> ⩛ + new Transition (6376, 6377), // &oS -> Ⓢ + new Transition (6380, 6381), // &Oscr -> 𝒪 + new Transition (6384, 6385), // &oscr -> ℴ + new Transition (6389, 6390), // Ø -> Ø + new Transition (6394, 6395), // ø -> ø + new Transition (6397, 6398), // &osol -> ⊘ + new Transition (6403, 6404), // Õ -> Õ + new Transition (6409, 6410), // õ -> õ + new Transition (6413, 6414), // &Otimes -> ⨷ + new Transition (6417, 6418), // &otimes -> ⊗ + new Transition (6420, 6421), // &otimesas -> ⨶ + new Transition (6424, 6425), // Ö -> Ö + new Transition (6428, 6429), // ö -> ö + new Transition (6433, 6434), // &ovbar -> ⌽ + new Transition (6440, 6441), // &OverBar -> ‾ + new Transition (6445, 6446), // &OverBrace -> ⏞ + new Transition (6449, 6450), // &OverBracket -> ⎴ + new Transition (6461, 6462), // &OverParenthesis -> ⏜ + new Transition (6465, 6466), // &par -> ∥ + new Transition (6467, 6468), // ¶ -> ¶ + new Transition (6472, 6473), // ¶llel -> ∥ + new Transition (6476, 6477), // &parsim -> ⫳ + new Transition (6478, 6479), // &parsl -> ⫽ + new Transition (6480, 6481), // &part -> ∂ + new Transition (6489, 6490), // &PartialD -> ∂ + new Transition (6492, 6493), // &Pcy -> П + new Transition (6495, 6496), // &pcy -> п + new Transition (6501, 6502), // &percnt -> % + new Transition (6505, 6506), // &period -> . + new Transition (6509, 6510), // &permil -> ‰ + new Transition (6511, 6512), // &perp -> ⊥ + new Transition (6516, 6517), // &pertenk -> ‱ + new Transition (6519, 6520), // &Pfr -> 𝔓 + new Transition (6522, 6523), // &pfr -> 𝔭 + new Transition (6525, 6526), // &Phi -> Φ + new Transition (6528, 6529), // &phi -> φ + new Transition (6530, 6531), // &phiv -> ϕ + new Transition (6535, 6536), // &phmmat -> ℳ + new Transition (6539, 6540), // &phone -> ☎ + new Transition (6541, 6542), // &Pi -> Π + new Transition (6543, 6544), // &pi -> π + new Transition (6551, 6552), // &pitchfork -> ⋔ + new Transition (6553, 6554), // &piv -> ϖ + new Transition (6559, 6560), // &planck -> ℏ + new Transition (6561, 6562), // &planckh -> ℎ + new Transition (6564, 6565), // &plankv -> ℏ + new Transition (6567, 6568), // &plus -> + + new Transition (6572, 6573), // &plusacir -> ⨣ + new Transition (6574, 6575), // &plusb -> ⊞ + new Transition (6578, 6579), // &pluscir -> ⨢ + new Transition (6581, 6582), // &plusdo -> ∔ + new Transition (6583, 6584), // &plusdu -> ⨥ + new Transition (6585, 6586), // &pluse -> ⩲ + new Transition (6594, 6595), // &PlusMinus -> ± + new Transition (6597, 6598), // ± -> ± + new Transition (6601, 6602), // &plussim -> ⨦ + new Transition (6605, 6606), // &plustwo -> ⨧ + new Transition (6607, 6608), // &pm -> ± + new Transition (6620, 6621), // &Poincareplane -> ℌ + new Transition (6628, 6629), // &pointint -> ⨕ + new Transition (6631, 6632), // &Popf -> ℙ + new Transition (6634, 6635), // &popf -> 𝕡 + new Transition (6638, 6639), // £ -> £ + new Transition (6640, 6641), // &Pr -> ⪻ + new Transition (6642, 6643), // &pr -> ≺ + new Transition (6645, 6646), // &prap -> ⪷ + new Transition (6649, 6650), // &prcue -> ≼ + new Transition (6651, 6652), // &prE -> ⪳ + new Transition (6653, 6654), // &pre -> ⪯ + new Transition (6655, 6656), // &prec -> ≺ + new Transition (6662, 6663), // &precapprox -> ⪷ + new Transition (6670, 6671), // &preccurlyeq -> ≼ + new Transition (6677, 6678), // &Precedes -> ≺ + new Transition (6683, 6684), // &PrecedesEqual -> ⪯ + new Transition (6694, 6695), // &PrecedesSlantEqual -> ≼ + new Transition (6700, 6701), // &PrecedesTilde -> ≾ + new Transition (6703, 6704), // &preceq -> ⪯ + new Transition (6711, 6712), // &precnapprox -> ⪹ + new Transition (6715, 6716), // &precneqq -> ⪵ + new Transition (6719, 6720), // &precnsim -> ⋨ + new Transition (6723, 6724), // &precsim -> ≾ + new Transition (6727, 6728), // &Prime -> ″ + new Transition (6731, 6732), // &prime -> ′ + new Transition (6733, 6734), // &primes -> ℙ + new Transition (6737, 6738), // &prnap -> ⪹ + new Transition (6739, 6740), // &prnE -> ⪵ + new Transition (6743, 6744), // &prnsim -> ⋨ + new Transition (6746, 6747), // &prod -> ∏ + new Transition (6752, 6753), // &Product -> ∏ + new Transition (6758, 6759), // &profalar -> ⌮ + new Transition (6763, 6764), // &profline -> ⌒ + new Transition (6768, 6769), // &profsurf -> ⌓ + new Transition (6770, 6771), // &prop -> ∝ + new Transition (6778, 6779), // &Proportion -> ∷ + new Transition (6781, 6782), // &Proportional -> ∝ + new Transition (6784, 6785), // &propto -> ∝ + new Transition (6788, 6789), // &prsim -> ≾ + new Transition (6793, 6794), // &prurel -> ⊰ + new Transition (6797, 6798), // &Pscr -> 𝒫 + new Transition (6801, 6802), // &pscr -> 𝓅 + new Transition (6803, 6804), // &Psi -> Ψ + new Transition (6805, 6806), // &psi -> ψ + new Transition (6811, 6812), // &puncsp ->   + new Transition (6815, 6816), // &Qfr -> 𝔔 + new Transition (6819, 6820), // &qfr -> 𝔮 + new Transition (6823, 6824), // &qint -> ⨌ + new Transition (6827, 6828), // &Qopf -> ℚ + new Transition (6831, 6832), // &qopf -> 𝕢 + new Transition (6837, 6838), // &qprime -> ⁗ + new Transition (6841, 6842), // &Qscr -> 𝒬 + new Transition (6845, 6846), // &qscr -> 𝓆 + new Transition (6856, 6857), // &quaternions -> ℍ + new Transition (6860, 6861), // &quatint -> ⨖ + new Transition (6864, 6865), // &quest -> ? + new Transition (6867, 6868), // &questeq -> ≟ + new Transition (6871, 6872), // " -> " + new Transition (6874, 6875), // " -> " + new Transition (6880, 6881), // &rAarr -> ⇛ + new Transition (6884, 6885), // &race -> ∽̱ + new Transition (6891, 6892), // &Racute -> Ŕ + new Transition (6895, 6896), // &racute -> ŕ + new Transition (6899, 6900), // &radic -> √ + new Transition (6906, 6907), // &raemptyv -> ⦳ + new Transition (6909, 6910), // &Rang -> ⟫ + new Transition (6912, 6913), // &rang -> ⟩ + new Transition (6914, 6915), // &rangd -> ⦒ + new Transition (6916, 6917), // &range -> ⦥ + new Transition (6919, 6920), // &rangle -> ⟩ + new Transition (6923, 6924), // » -> » + new Transition (6926, 6927), // &Rarr -> ↠ + new Transition (6929, 6930), // &rArr -> ⇒ + new Transition (6932, 6933), // &rarr -> → + new Transition (6935, 6936), // &rarrap -> ⥵ + new Transition (6937, 6938), // &rarrb -> ⇥ + new Transition (6940, 6941), // &rarrbfs -> ⤠ + new Transition (6942, 6943), // &rarrc -> ⤳ + new Transition (6945, 6946), // &rarrfs -> ⤞ + new Transition (6948, 6949), // &rarrhk -> ↪ + new Transition (6951, 6952), // &rarrlp -> ↬ + new Transition (6954, 6955), // &rarrpl -> ⥅ + new Transition (6958, 6959), // &rarrsim -> ⥴ + new Transition (6961, 6962), // &Rarrtl -> ⤖ + new Transition (6964, 6965), // &rarrtl -> ↣ + new Transition (6966, 6967), // &rarrw -> ↝ + new Transition (6971, 6972), // &rAtail -> ⤜ + new Transition (6976, 6977), // &ratail -> ⤚ + new Transition (6979, 6980), // &ratio -> ∶ + new Transition (6984, 6985), // &rationals -> ℚ + new Transition (6989, 6990), // &RBarr -> ⤐ + new Transition (6994, 6995), // &rBarr -> ⤏ + new Transition (6999, 7000), // &rbarr -> ⤍ + new Transition (7003, 7004), // &rbbrk -> ❳ + new Transition (7008, 7009), // &rbrace -> } + new Transition (7010, 7011), // &rbrack -> ] + new Transition (7013, 7014), // &rbrke -> ⦌ + new Transition (7017, 7018), // &rbrksld -> ⦎ + new Transition (7019, 7020), // &rbrkslu -> ⦐ + new Transition (7025, 7026), // &Rcaron -> Ř + new Transition (7031, 7032), // &rcaron -> ř + new Transition (7036, 7037), // &Rcedil -> Ŗ + new Transition (7041, 7042), // &rcedil -> ŗ + new Transition (7044, 7045), // &rceil -> ⌉ + new Transition (7047, 7048), // &rcub -> } + new Transition (7049, 7050), // &Rcy -> Р + new Transition (7051, 7052), // &rcy -> р + new Transition (7055, 7056), // &rdca -> ⤷ + new Transition (7061, 7062), // &rdldhar -> ⥩ + new Transition (7065, 7066), // &rdquo -> ” + new Transition (7067, 7068), // &rdquor -> ” + new Transition (7070, 7071), // &rdsh -> ↳ + new Transition (7072, 7073), // &Re -> ℜ + new Transition (7076, 7077), // &real -> ℜ + new Transition (7080, 7081), // &realine -> ℛ + new Transition (7085, 7086), // &realpart -> ℜ + new Transition (7087, 7088), // &reals -> ℝ + new Transition (7090, 7091), // &rect -> ▭ + new Transition (7093, 7094), // ® -> ® + new Transition (7095, 7096), // ® -> ® + new Transition (7108, 7109), // &ReverseElement -> ∋ + new Transition (7119, 7120), // &ReverseEquilibrium -> ⇋ + new Transition (7133, 7134), // &ReverseUpEquilibrium -> ⥯ + new Transition (7139, 7140), // &rfisht -> ⥽ + new Transition (7144, 7145), // &rfloor -> ⌋ + new Transition (7147, 7148), // &Rfr -> ℜ + new Transition (7149, 7150), // &rfr -> 𝔯 + new Transition (7153, 7154), // &rHar -> ⥤ + new Transition (7158, 7159), // &rhard -> ⇁ + new Transition (7160, 7161), // &rharu -> ⇀ + new Transition (7162, 7163), // &rharul -> ⥬ + new Transition (7165, 7166), // &Rho -> Ρ + new Transition (7167, 7168), // &rho -> ρ + new Transition (7169, 7170), // &rhov -> ϱ + new Transition (7186, 7187), // &RightAngleBracket -> ⟩ + new Transition (7191, 7192), // &RightArrow -> → + new Transition (7197, 7198), // &Rightarrow -> ⇒ + new Transition (7207, 7208), // &rightarrow -> → + new Transition (7211, 7212), // &RightArrowBar -> ⇥ + new Transition (7221, 7222), // &RightArrowLeftArrow -> ⇄ + new Transition (7226, 7227), // &rightarrowtail -> ↣ + new Transition (7234, 7235), // &RightCeiling -> ⌉ + new Transition (7248, 7249), // &RightDoubleBracket -> ⟧ + new Transition (7260, 7261), // &RightDownTeeVector -> ⥝ + new Transition (7267, 7268), // &RightDownVector -> ⇂ + new Transition (7271, 7272), // &RightDownVectorBar -> ⥕ + new Transition (7277, 7278), // &RightFloor -> ⌋ + new Transition (7289, 7290), // &rightharpoondown -> ⇁ + new Transition (7292, 7293), // &rightharpoonup -> ⇀ + new Transition (7303, 7304), // &rightleftarrows -> ⇄ + new Transition (7312, 7313), // &rightleftharpoons -> ⇌ + new Transition (7324, 7325), // &rightrightarrows -> ⇉ + new Transition (7335, 7336), // &rightsquigarrow -> ↝ + new Transition (7339, 7340), // &RightTee -> ⊢ + new Transition (7345, 7346), // &RightTeeArrow -> ↦ + new Transition (7352, 7353), // &RightTeeVector -> ⥛ + new Transition (7363, 7364), // &rightthreetimes -> ⋌ + new Transition (7371, 7372), // &RightTriangle -> ⊳ + new Transition (7375, 7376), // &RightTriangleBar -> ⧐ + new Transition (7381, 7382), // &RightTriangleEqual -> ⊵ + new Transition (7394, 7395), // &RightUpDownVector -> ⥏ + new Transition (7404, 7405), // &RightUpTeeVector -> ⥜ + new Transition (7411, 7412), // &RightUpVector -> ↾ + new Transition (7415, 7416), // &RightUpVectorBar -> ⥔ + new Transition (7422, 7423), // &RightVector -> ⇀ + new Transition (7426, 7427), // &RightVectorBar -> ⥓ + new Transition (7429, 7430), // &ring -> ˚ + new Transition (7440, 7441), // &risingdotseq -> ≓ + new Transition (7445, 7446), // &rlarr -> ⇄ + new Transition (7449, 7450), // &rlhar -> ⇌ + new Transition (7451, 7452), // &rlm -> ‏ + new Transition (7457, 7458), // &rmoust -> ⎱ + new Transition (7462, 7463), // &rmoustache -> ⎱ + new Transition (7467, 7468), // &rnmid -> ⫮ + new Transition (7472, 7473), // &roang -> ⟭ + new Transition (7475, 7476), // &roarr -> ⇾ + new Transition (7479, 7480), // &robrk -> ⟧ + new Transition (7483, 7484), // &ropar -> ⦆ + new Transition (7487, 7488), // &Ropf -> ℝ + new Transition (7489, 7490), // &ropf -> 𝕣 + new Transition (7493, 7494), // &roplus -> ⨮ + new Transition (7499, 7500), // &rotimes -> ⨵ + new Transition (7510, 7511), // &RoundImplies -> ⥰ + new Transition (7514, 7515), // &rpar -> ) + new Transition (7517, 7518), // &rpargt -> ⦔ + new Transition (7524, 7525), // &rppolint -> ⨒ + new Transition (7529, 7530), // &rrarr -> ⇉ + new Transition (7540, 7541), // &Rrightarrow -> ⇛ + new Transition (7546, 7547), // &rsaquo -> › + new Transition (7550, 7551), // &Rscr -> ℛ + new Transition (7553, 7554), // &rscr -> 𝓇 + new Transition (7555, 7556), // &Rsh -> ↱ + new Transition (7557, 7558), // &rsh -> ↱ + new Transition (7560, 7561), // &rsqb -> ] + new Transition (7563, 7564), // &rsquo -> ’ + new Transition (7565, 7566), // &rsquor -> ’ + new Transition (7571, 7572), // &rthree -> ⋌ + new Transition (7576, 7577), // &rtimes -> ⋊ + new Transition (7579, 7580), // &rtri -> ▹ + new Transition (7581, 7582), // &rtrie -> ⊵ + new Transition (7583, 7584), // &rtrif -> ▸ + new Transition (7588, 7589), // &rtriltri -> ⧎ + new Transition (7599, 7600), // &RuleDelayed -> ⧴ + new Transition (7606, 7607), // &ruluhar -> ⥨ + new Transition (7608, 7609), // &rx -> ℞ + new Transition (7615, 7616), // &Sacute -> Ś + new Transition (7622, 7623), // &sacute -> ś + new Transition (7627, 7628), // &sbquo -> ‚ + new Transition (7629, 7630), // &Sc -> ⪼ + new Transition (7631, 7632), // &sc -> ≻ + new Transition (7634, 7635), // &scap -> ⪸ + new Transition (7639, 7640), // &Scaron -> Š + new Transition (7643, 7644), // &scaron -> š + new Transition (7647, 7648), // &sccue -> ≽ + new Transition (7649, 7650), // &scE -> ⪴ + new Transition (7651, 7652), // &sce -> ⪰ + new Transition (7656, 7657), // &Scedil -> Ş + new Transition (7660, 7661), // &scedil -> ş + new Transition (7664, 7665), // &Scirc -> Ŝ + new Transition (7668, 7669), // &scirc -> ŝ + new Transition (7672, 7673), // &scnap -> ⪺ + new Transition (7674, 7675), // &scnE -> ⪶ + new Transition (7678, 7679), // &scnsim -> ⋩ + new Transition (7685, 7686), // &scpolint -> ⨓ + new Transition (7689, 7690), // &scsim -> ≿ + new Transition (7691, 7692), // &Scy -> С + new Transition (7693, 7694), // &scy -> с + new Transition (7697, 7698), // &sdot -> ⋅ + new Transition (7699, 7700), // &sdotb -> ⊡ + new Transition (7701, 7702), // &sdote -> ⩦ + new Transition (7707, 7708), // &searhk -> ⤥ + new Transition (7711, 7712), // &seArr -> ⇘ + new Transition (7713, 7714), // &searr -> ↘ + new Transition (7716, 7717), // &searrow -> ↘ + new Transition (7719, 7720), // § -> § + new Transition (7722, 7723), // &semi -> ; + new Transition (7727, 7728), // &seswar -> ⤩ + new Transition (7734, 7735), // &setminus -> ∖ + new Transition (7736, 7737), // &setmn -> ∖ + new Transition (7739, 7740), // &sext -> ✶ + new Transition (7742, 7743), // &Sfr -> 𝔖 + new Transition (7745, 7746), // &sfr -> 𝔰 + new Transition (7749, 7750), // &sfrown -> ⌢ + new Transition (7754, 7755), // &sharp -> ♯ + new Transition (7760, 7761), // &SHCHcy -> Щ + new Transition (7765, 7766), // &shchcy -> щ + new Transition (7768, 7769), // &SHcy -> Ш + new Transition (7770, 7771), // &shcy -> ш + new Transition (7784, 7785), // &ShortDownArrow -> ↓ + new Transition (7794, 7795), // &ShortLeftArrow -> ← + new Transition (7801, 7802), // &shortmid -> ∣ + new Transition (7810, 7811), // &shortparallel -> ∥ + new Transition (7821, 7822), // &ShortRightArrow -> → + new Transition (7829, 7830), // &ShortUpArrow -> ↑ + new Transition (7831, 7832), // ­ -> ­ + new Transition (7836, 7837), // &Sigma -> Σ + new Transition (7841, 7842), // &sigma -> σ + new Transition (7843, 7844), // &sigmaf -> ς + new Transition (7845, 7846), // &sigmav -> ς + new Transition (7847, 7848), // &sim -> ∼ + new Transition (7851, 7852), // &simdot -> ⩪ + new Transition (7853, 7854), // &sime -> ≃ + new Transition (7855, 7856), // &simeq -> ≃ + new Transition (7857, 7858), // &simg -> ⪞ + new Transition (7859, 7860), // &simgE -> ⪠ + new Transition (7861, 7862), // &siml -> ⪝ + new Transition (7863, 7864), // &simlE -> ⪟ + new Transition (7866, 7867), // &simne -> ≆ + new Transition (7871, 7872), // &simplus -> ⨤ + new Transition (7876, 7877), // &simrarr -> ⥲ + new Transition (7881, 7882), // &slarr -> ← + new Transition (7892, 7893), // &SmallCircle -> ∘ + new Transition (7905, 7906), // &smallsetminus -> ∖ + new Transition (7909, 7910), // &smashp -> ⨳ + new Transition (7916, 7917), // &smeparsl -> ⧤ + new Transition (7919, 7920), // &smid -> ∣ + new Transition (7922, 7923), // &smile -> ⌣ + new Transition (7924, 7925), // &smt -> ⪪ + new Transition (7926, 7927), // &smte -> ⪬ + new Transition (7928, 7929), // &smtes -> ⪬︀ + new Transition (7934, 7935), // &SOFTcy -> Ь + new Transition (7940, 7941), // &softcy -> ь + new Transition (7942, 7943), // &sol -> / + new Transition (7944, 7945), // &solb -> ⧄ + new Transition (7947, 7948), // &solbar -> ⌿ + new Transition (7951, 7952), // &Sopf -> 𝕊 + new Transition (7954, 7955), // &sopf -> 𝕤 + new Transition (7960, 7961), // &spades -> ♠ + new Transition (7964, 7965), // &spadesuit -> ♠ + new Transition (7966, 7967), // &spar -> ∥ + new Transition (7971, 7972), // &sqcap -> ⊓ + new Transition (7973, 7974), // &sqcaps -> ⊓︀ + new Transition (7976, 7977), // &sqcup -> ⊔ + new Transition (7978, 7979), // &sqcups -> ⊔︀ + new Transition (7982, 7983), // &Sqrt -> √ + new Transition (7986, 7987), // &sqsub -> ⊏ + new Transition (7988, 7989), // &sqsube -> ⊑ + new Transition (7992, 7993), // &sqsubset -> ⊏ + new Transition (7995, 7996), // &sqsubseteq -> ⊑ + new Transition (7997, 7998), // &sqsup -> ⊐ + new Transition (7999, 8000), // &sqsupe -> ⊒ + new Transition (8003, 8004), // &sqsupset -> ⊐ + new Transition (8006, 8007), // &sqsupseteq -> ⊒ + new Transition (8008, 8009), // &squ -> □ + new Transition (8013, 8014), // &Square -> □ + new Transition (8017, 8018), // &square -> □ + new Transition (8030, 8031), // &SquareIntersection -> ⊓ + new Transition (8037, 8038), // &SquareSubset -> ⊏ + new Transition (8043, 8044), // &SquareSubsetEqual -> ⊑ + new Transition (8050, 8051), // &SquareSuperset -> ⊐ + new Transition (8056, 8057), // &SquareSupersetEqual -> ⊒ + new Transition (8062, 8063), // &SquareUnion -> ⊔ + new Transition (8064, 8065), // &squarf -> ▪ + new Transition (8066, 8067), // &squf -> ▪ + new Transition (8071, 8072), // &srarr -> → + new Transition (8075, 8076), // &Sscr -> 𝒮 + new Transition (8079, 8080), // &sscr -> 𝓈 + new Transition (8084, 8085), // &ssetmn -> ∖ + new Transition (8089, 8090), // &ssmile -> ⌣ + new Transition (8094, 8095), // &sstarf -> ⋆ + new Transition (8098, 8099), // &Star -> ⋆ + new Transition (8102, 8103), // &star -> ☆ + new Transition (8104, 8105), // &starf -> ★ + new Transition (8118, 8119), // &straightepsilon -> ϵ + new Transition (8122, 8123), // &straightphi -> ϕ + new Transition (8125, 8126), // &strns -> ¯ + new Transition (8128, 8129), // &Sub -> ⋐ + new Transition (8131, 8132), // &sub -> ⊂ + new Transition (8135, 8136), // &subdot -> ⪽ + new Transition (8137, 8138), // &subE -> ⫅ + new Transition (8139, 8140), // &sube -> ⊆ + new Transition (8143, 8144), // &subedot -> ⫃ + new Transition (8148, 8149), // &submult -> ⫁ + new Transition (8151, 8152), // &subnE -> ⫋ + new Transition (8153, 8154), // &subne -> ⊊ + new Transition (8158, 8159), // &subplus -> ⪿ + new Transition (8163, 8164), // &subrarr -> ⥹ + new Transition (8167, 8168), // &Subset -> ⋐ + new Transition (8171, 8172), // &subset -> ⊂ + new Transition (8174, 8175), // &subseteq -> ⊆ + new Transition (8176, 8177), // &subseteqq -> ⫅ + new Transition (8182, 8183), // &SubsetEqual -> ⊆ + new Transition (8186, 8187), // &subsetneq -> ⊊ + new Transition (8188, 8189), // &subsetneqq -> ⫋ + new Transition (8191, 8192), // &subsim -> ⫇ + new Transition (8194, 8195), // &subsub -> ⫕ + new Transition (8196, 8197), // &subsup -> ⫓ + new Transition (8199, 8200), // &succ -> ≻ + new Transition (8206, 8207), // &succapprox -> ⪸ + new Transition (8214, 8215), // &succcurlyeq -> ≽ + new Transition (8221, 8222), // &Succeeds -> ≻ + new Transition (8227, 8228), // &SucceedsEqual -> ⪰ + new Transition (8238, 8239), // &SucceedsSlantEqual -> ≽ + new Transition (8244, 8245), // &SucceedsTilde -> ≿ + new Transition (8247, 8248), // &succeq -> ⪰ + new Transition (8255, 8256), // &succnapprox -> ⪺ + new Transition (8259, 8260), // &succneqq -> ⪶ + new Transition (8263, 8264), // &succnsim -> ⋩ + new Transition (8267, 8268), // &succsim -> ≿ + new Transition (8273, 8274), // &SuchThat -> ∋ + new Transition (8275, 8276), // &Sum -> ∑ + new Transition (8277, 8278), // &sum -> ∑ + new Transition (8280, 8281), // &sung -> ♪ + new Transition (8282, 8283), // &Sup -> ⋑ + new Transition (8284, 8285), // &sup -> ⊃ + new Transition (8286, 8287), // ¹ -> ¹ + new Transition (8288, 8289), // ² -> ² + new Transition (8290, 8291), // ³ -> ³ + new Transition (8294, 8295), // &supdot -> ⪾ + new Transition (8298, 8299), // &supdsub -> ⫘ + new Transition (8300, 8301), // &supE -> ⫆ + new Transition (8302, 8303), // &supe -> ⊇ + new Transition (8306, 8307), // &supedot -> ⫄ + new Transition (8312, 8313), // &Superset -> ⊃ + new Transition (8318, 8319), // &SupersetEqual -> ⊇ + new Transition (8323, 8324), // &suphsol -> ⟉ + new Transition (8326, 8327), // &suphsub -> ⫗ + new Transition (8331, 8332), // &suplarr -> ⥻ + new Transition (8336, 8337), // &supmult -> ⫂ + new Transition (8339, 8340), // &supnE -> ⫌ + new Transition (8341, 8342), // &supne -> ⊋ + new Transition (8346, 8347), // &supplus -> ⫀ + new Transition (8350, 8351), // &Supset -> ⋑ + new Transition (8354, 8355), // &supset -> ⊃ + new Transition (8357, 8358), // &supseteq -> ⊇ + new Transition (8359, 8360), // &supseteqq -> ⫆ + new Transition (8363, 8364), // &supsetneq -> ⊋ + new Transition (8365, 8366), // &supsetneqq -> ⫌ + new Transition (8368, 8369), // &supsim -> ⫈ + new Transition (8371, 8372), // &supsub -> ⫔ + new Transition (8373, 8374), // &supsup -> ⫖ + new Transition (8379, 8380), // &swarhk -> ⤦ + new Transition (8383, 8384), // &swArr -> ⇙ + new Transition (8385, 8386), // &swarr -> ↙ + new Transition (8388, 8389), // &swarrow -> ↙ + new Transition (8393, 8394), // &swnwar -> ⤪ + new Transition (8398, 8399), // ß -> ß + new Transition (8402, 8403), // &Tab -> + new Transition (8409, 8410), // &target -> ⌖ + new Transition (8411, 8412), // &Tau -> Τ + new Transition (8413, 8414), // &tau -> τ + new Transition (8417, 8418), // &tbrk -> ⎴ + new Transition (8423, 8424), // &Tcaron -> Ť + new Transition (8429, 8430), // &tcaron -> ť + new Transition (8434, 8435), // &Tcedil -> Ţ + new Transition (8439, 8440), // &tcedil -> ţ + new Transition (8441, 8442), // &Tcy -> Т + new Transition (8443, 8444), // &tcy -> т + new Transition (8447, 8448), // &tdot -> ⃛ + new Transition (8453, 8454), // &telrec -> ⌕ + new Transition (8456, 8457), // &Tfr -> 𝔗 + new Transition (8459, 8460), // &tfr -> 𝔱 + new Transition (8465, 8466), // &there4 -> ∴ + new Transition (8474, 8475), // &Therefore -> ∴ + new Transition (8479, 8480), // &therefore -> ∴ + new Transition (8482, 8483), // &Theta -> Θ + new Transition (8485, 8486), // &theta -> θ + new Transition (8489, 8490), // &thetasym -> ϑ + new Transition (8491, 8492), // &thetav -> ϑ + new Transition (8501, 8502), // &thickapprox -> ≈ + new Transition (8505, 8506), // &thicksim -> ∼ + new Transition (8514, 8515), // &ThickSpace ->    + new Transition (8518, 8519), // &thinsp ->   + new Transition (8525, 8526), // &ThinSpace ->   + new Transition (8529, 8530), // &thkap -> ≈ + new Transition (8533, 8534), // &thksim -> ∼ + new Transition (8538, 8539), // Þ -> Þ + new Transition (8542, 8543), // þ -> þ + new Transition (8547, 8548), // &Tilde -> ∼ + new Transition (8552, 8553), // &tilde -> ˜ + new Transition (8558, 8559), // &TildeEqual -> ≃ + new Transition (8568, 8569), // &TildeFullEqual -> ≅ + new Transition (8574, 8575), // &TildeTilde -> ≈ + new Transition (8578, 8579), // × -> × + new Transition (8580, 8581), // ×b -> ⊠ + new Transition (8583, 8584), // ×bar -> ⨱ + new Transition (8585, 8586), // ×d -> ⨰ + new Transition (8588, 8589), // &tint -> ∭ + new Transition (8592, 8593), // &toea -> ⤨ + new Transition (8594, 8595), // &top -> ⊤ + new Transition (8598, 8599), // &topbot -> ⌶ + new Transition (8602, 8603), // &topcir -> ⫱ + new Transition (8606, 8607), // &Topf -> 𝕋 + new Transition (8608, 8609), // &topf -> 𝕥 + new Transition (8612, 8613), // &topfork -> ⫚ + new Transition (8615, 8616), // &tosa -> ⤩ + new Transition (8621, 8622), // &tprime -> ‴ + new Transition (8626, 8627), // &TRADE -> ™ + new Transition (8631, 8632), // &trade -> ™ + new Transition (8638, 8639), // &triangle -> ▵ + new Transition (8643, 8644), // &triangledown -> ▿ + new Transition (8648, 8649), // &triangleleft -> ◃ + new Transition (8651, 8652), // &trianglelefteq -> ⊴ + new Transition (8653, 8654), // &triangleq -> ≜ + new Transition (8659, 8660), // &triangleright -> ▹ + new Transition (8662, 8663), // &trianglerighteq -> ⊵ + new Transition (8666, 8667), // &tridot -> ◬ + new Transition (8668, 8669), // &trie -> ≜ + new Transition (8674, 8675), // &triminus -> ⨺ + new Transition (8683, 8684), // &TripleDot -> ⃛ + new Transition (8688, 8689), // &triplus -> ⨹ + new Transition (8691, 8692), // &trisb -> ⧍ + new Transition (8696, 8697), // &tritime -> ⨻ + new Transition (8703, 8704), // &trpezium -> ⏢ + new Transition (8707, 8708), // &Tscr -> 𝒯 + new Transition (8711, 8712), // &tscr -> 𝓉 + new Transition (8715, 8716), // &TScy -> Ц + new Transition (8717, 8718), // &tscy -> ц + new Transition (8721, 8722), // &TSHcy -> Ћ + new Transition (8725, 8726), // &tshcy -> ћ + new Transition (8730, 8731), // &Tstrok -> Ŧ + new Transition (8735, 8736), // &tstrok -> ŧ + new Transition (8740, 8741), // &twixt -> ≬ + new Transition (8755, 8756), // &twoheadleftarrow -> ↞ + new Transition (8766, 8767), // &twoheadrightarrow -> ↠ + new Transition (8773, 8774), // Ú -> Ú + new Transition (8780, 8781), // ú -> ú + new Transition (8783, 8784), // &Uarr -> ↟ + new Transition (8787, 8788), // &uArr -> ⇑ + new Transition (8790, 8791), // &uarr -> ↑ + new Transition (8795, 8796), // &Uarrocir -> ⥉ + new Transition (8800, 8801), // &Ubrcy -> Ў + new Transition (8805, 8806), // &ubrcy -> ў + new Transition (8809, 8810), // &Ubreve -> Ŭ + new Transition (8813, 8814), // &ubreve -> ŭ + new Transition (8818, 8819), // Û -> Û + new Transition (8823, 8824), // û -> û + new Transition (8825, 8826), // &Ucy -> У + new Transition (8827, 8828), // &ucy -> у + new Transition (8832, 8833), // &udarr -> ⇅ + new Transition (8838, 8839), // &Udblac -> Ű + new Transition (8843, 8844), // &udblac -> ű + new Transition (8847, 8848), // &udhar -> ⥮ + new Transition (8853, 8854), // &ufisht -> ⥾ + new Transition (8856, 8857), // &Ufr -> 𝔘 + new Transition (8858, 8859), // &ufr -> 𝔲 + new Transition (8864, 8865), // Ù -> Ù + new Transition (8870, 8871), // ù -> ù + new Transition (8874, 8875), // &uHar -> ⥣ + new Transition (8879, 8880), // &uharl -> ↿ + new Transition (8881, 8882), // &uharr -> ↾ + new Transition (8885, 8886), // &uhblk -> ▀ + new Transition (8891, 8892), // &ulcorn -> ⌜ + new Transition (8894, 8895), // &ulcorner -> ⌜ + new Transition (8898, 8899), // &ulcrop -> ⌏ + new Transition (8902, 8903), // &ultri -> ◸ + new Transition (8907, 8908), // &Umacr -> Ū + new Transition (8912, 8913), // &umacr -> ū + new Transition (8914, 8915), // ¨ -> ¨ + new Transition (8922, 8923), // &UnderBar -> _ + new Transition (8927, 8928), // &UnderBrace -> ⏟ + new Transition (8931, 8932), // &UnderBracket -> ⎵ + new Transition (8943, 8944), // &UnderParenthesis -> ⏝ + new Transition (8947, 8948), // &Union -> ⋃ + new Transition (8952, 8953), // &UnionPlus -> ⊎ + new Transition (8957, 8958), // &Uogon -> Ų + new Transition (8962, 8963), // &uogon -> ų + new Transition (8965, 8966), // &Uopf -> 𝕌 + new Transition (8968, 8969), // &uopf -> 𝕦 + new Transition (8975, 8976), // &UpArrow -> ↑ + new Transition (8981, 8982), // &Uparrow -> ⇑ + new Transition (8988, 8989), // &uparrow -> ↑ + new Transition (8992, 8993), // &UpArrowBar -> ⤒ + new Transition (9002, 9003), // &UpArrowDownArrow -> ⇅ + new Transition (9012, 9013), // &UpDownArrow -> ↕ + new Transition (9022, 9023), // &Updownarrow -> ⇕ + new Transition (9032, 9033), // &updownarrow -> ↕ + new Transition (9044, 9045), // &UpEquilibrium -> ⥮ + new Transition (9056, 9057), // &upharpoonleft -> ↿ + new Transition (9062, 9063), // &upharpoonright -> ↾ + new Transition (9066, 9067), // &uplus -> ⊎ + new Transition (9079, 9080), // &UpperLeftArrow -> ↖ + new Transition (9090, 9091), // &UpperRightArrow -> ↗ + new Transition (9093, 9094), // &Upsi -> ϒ + new Transition (9096, 9097), // &upsi -> υ + new Transition (9098, 9099), // &upsih -> ϒ + new Transition (9102, 9103), // &Upsilon -> Υ + new Transition (9106, 9107), // &upsilon -> υ + new Transition (9110, 9111), // &UpTee -> ⊥ + new Transition (9116, 9117), // &UpTeeArrow -> ↥ + new Transition (9125, 9126), // &upuparrows -> ⇈ + new Transition (9131, 9132), // &urcorn -> ⌝ + new Transition (9134, 9135), // &urcorner -> ⌝ + new Transition (9138, 9139), // &urcrop -> ⌎ + new Transition (9143, 9144), // &Uring -> Ů + new Transition (9147, 9148), // &uring -> ů + new Transition (9151, 9152), // &urtri -> ◹ + new Transition (9155, 9156), // &Uscr -> 𝒰 + new Transition (9159, 9160), // &uscr -> 𝓊 + new Transition (9164, 9165), // &utdot -> ⋰ + new Transition (9170, 9171), // &Utilde -> Ũ + new Transition (9175, 9176), // &utilde -> ũ + new Transition (9178, 9179), // &utri -> ▵ + new Transition (9180, 9181), // &utrif -> ▴ + new Transition (9185, 9186), // &uuarr -> ⇈ + new Transition (9189, 9190), // Ü -> Ü + new Transition (9192, 9193), // ü -> ü + new Transition (9199, 9200), // &uwangle -> ⦧ + new Transition (9206, 9207), // &vangrt -> ⦜ + new Transition (9215, 9216), // &varepsilon -> ϵ + new Transition (9221, 9222), // &varkappa -> ϰ + new Transition (9229, 9230), // &varnothing -> ∅ + new Transition (9233, 9234), // &varphi -> ϕ + new Transition (9235, 9236), // &varpi -> ϖ + new Transition (9241, 9242), // &varpropto -> ∝ + new Transition (9245, 9246), // &vArr -> ⇕ + new Transition (9247, 9248), // &varr -> ↕ + new Transition (9250, 9251), // &varrho -> ϱ + new Transition (9256, 9257), // &varsigma -> ς + new Transition (9265, 9266), // &varsubsetneq -> ⊊︀ + new Transition (9267, 9268), // &varsubsetneqq -> ⫋︀ + new Transition (9275, 9276), // &varsupsetneq -> ⊋︀ + new Transition (9277, 9278), // &varsupsetneqq -> ⫌︀ + new Transition (9283, 9284), // &vartheta -> ϑ + new Transition (9295, 9296), // &vartriangleleft -> ⊲ + new Transition (9301, 9302), // &vartriangleright -> ⊳ + new Transition (9306, 9307), // &Vbar -> ⫫ + new Transition (9310, 9311), // &vBar -> ⫨ + new Transition (9312, 9313), // &vBarv -> ⫩ + new Transition (9315, 9316), // &Vcy -> В + new Transition (9318, 9319), // &vcy -> в + new Transition (9323, 9324), // &VDash -> ⊫ + new Transition (9328, 9329), // &Vdash -> ⊩ + new Transition (9333, 9334), // &vDash -> ⊨ + new Transition (9338, 9339), // &vdash -> ⊢ + new Transition (9340, 9341), // &Vdashl -> ⫦ + new Transition (9343, 9344), // &Vee -> ⋁ + new Transition (9346, 9347), // &vee -> ∨ + new Transition (9350, 9351), // &veebar -> ⊻ + new Transition (9353, 9354), // &veeeq -> ≚ + new Transition (9358, 9359), // &vellip -> ⋮ + new Transition (9363, 9364), // &Verbar -> ‖ + new Transition (9368, 9369), // &verbar -> | + new Transition (9370, 9371), // &Vert -> ‖ + new Transition (9372, 9373), // &vert -> | + new Transition (9380, 9381), // &VerticalBar -> ∣ + new Transition (9385, 9386), // &VerticalLine -> | + new Transition (9395, 9396), // &VerticalSeparator -> ❘ + new Transition (9401, 9402), // &VerticalTilde -> ≀ + new Transition (9412, 9413), // &VeryThinSpace ->   + new Transition (9415, 9416), // &Vfr -> 𝔙 + new Transition (9418, 9419), // &vfr -> 𝔳 + new Transition (9423, 9424), // &vltri -> ⊲ + new Transition (9428, 9429), // &vnsub -> ⊂⃒ + new Transition (9430, 9431), // &vnsup -> ⊃⃒ + new Transition (9434, 9435), // &Vopf -> 𝕍 + new Transition (9438, 9439), // &vopf -> 𝕧 + new Transition (9443, 9444), // &vprop -> ∝ + new Transition (9448, 9449), // &vrtri -> ⊳ + new Transition (9452, 9453), // &Vscr -> 𝒱 + new Transition (9456, 9457), // &vscr -> 𝓋 + new Transition (9461, 9462), // &vsubnE -> ⫋︀ + new Transition (9463, 9464), // &vsubne -> ⊊︀ + new Transition (9467, 9468), // &vsupnE -> ⫌︀ + new Transition (9469, 9470), // &vsupne -> ⊋︀ + new Transition (9475, 9476), // &Vvdash -> ⊪ + new Transition (9482, 9483), // &vzigzag -> ⦚ + new Transition (9488, 9489), // &Wcirc -> Ŵ + new Transition (9494, 9495), // &wcirc -> ŵ + new Transition (9500, 9501), // &wedbar -> ⩟ + new Transition (9505, 9506), // &Wedge -> ⋀ + new Transition (9508, 9509), // &wedge -> ∧ + new Transition (9510, 9511), // &wedgeq -> ≙ + new Transition (9515, 9516), // &weierp -> ℘ + new Transition (9518, 9519), // &Wfr -> 𝔚 + new Transition (9521, 9522), // &wfr -> 𝔴 + new Transition (9525, 9526), // &Wopf -> 𝕎 + new Transition (9529, 9530), // &wopf -> 𝕨 + new Transition (9531, 9532), // &wp -> ℘ + new Transition (9533, 9534), // &wr -> ≀ + new Transition (9538, 9539), // &wreath -> ≀ + new Transition (9542, 9543), // &Wscr -> 𝒲 + new Transition (9546, 9547), // &wscr -> 𝓌 + new Transition (9551, 9552), // &xcap -> ⋂ + new Transition (9555, 9556), // &xcirc -> ◯ + new Transition (9558, 9559), // &xcup -> ⋃ + new Transition (9563, 9564), // &xdtri -> ▽ + new Transition (9567, 9568), // &Xfr -> 𝔛 + new Transition (9570, 9571), // &xfr -> 𝔵 + new Transition (9575, 9576), // &xhArr -> ⟺ + new Transition (9579, 9580), // &xharr -> ⟷ + new Transition (9581, 9582), // &Xi -> Ξ + new Transition (9583, 9584), // &xi -> ξ + new Transition (9588, 9589), // &xlArr -> ⟸ + new Transition (9592, 9593), // &xlarr -> ⟵ + new Transition (9596, 9597), // &xmap -> ⟼ + new Transition (9600, 9601), // &xnis -> ⋻ + new Transition (9605, 9606), // &xodot -> ⨀ + new Transition (9609, 9610), // &Xopf -> 𝕏 + new Transition (9612, 9613), // &xopf -> 𝕩 + new Transition (9616, 9617), // &xoplus -> ⨁ + new Transition (9621, 9622), // &xotime -> ⨂ + new Transition (9626, 9627), // &xrArr -> ⟹ + new Transition (9630, 9631), // &xrarr -> ⟶ + new Transition (9634, 9635), // &Xscr -> 𝒳 + new Transition (9638, 9639), // &xscr -> 𝓍 + new Transition (9643, 9644), // &xsqcup -> ⨆ + new Transition (9649, 9650), // &xuplus -> ⨄ + new Transition (9653, 9654), // &xutri -> △ + new Transition (9657, 9658), // &xvee -> ⋁ + new Transition (9663, 9664), // &xwedge -> ⋀ + new Transition (9670, 9671), // Ý -> Ý + new Transition (9677, 9678), // ý -> ý + new Transition (9681, 9682), // &YAcy -> Я + new Transition (9683, 9684), // &yacy -> я + new Transition (9688, 9689), // &Ycirc -> Ŷ + new Transition (9693, 9694), // &ycirc -> ŷ + new Transition (9695, 9696), // &Ycy -> Ы + new Transition (9697, 9698), // &ycy -> ы + new Transition (9700, 9701), // ¥ -> ¥ + new Transition (9703, 9704), // &Yfr -> 𝔜 + new Transition (9706, 9707), // &yfr -> 𝔶 + new Transition (9710, 9711), // &YIcy -> Ї + new Transition (9714, 9715), // &yicy -> ї + new Transition (9718, 9719), // &Yopf -> 𝕐 + new Transition (9722, 9723), // &yopf -> 𝕪 + new Transition (9726, 9727), // &Yscr -> 𝒴 + new Transition (9730, 9731), // &yscr -> 𝓎 + new Transition (9734, 9735), // &YUcy -> Ю + new Transition (9738, 9739), // &yucy -> ю + new Transition (9742, 9743), // &Yuml -> Ÿ + new Transition (9745, 9746), // ÿ -> ÿ + new Transition (9752, 9753), // &Zacute -> Ź + new Transition (9759, 9760), // &zacute -> ź + new Transition (9765, 9766), // &Zcaron -> Ž + new Transition (9771, 9772), // &zcaron -> ž + new Transition (9773, 9774), // &Zcy -> З + new Transition (9775, 9776), // &zcy -> з + new Transition (9779, 9780), // &Zdot -> Ż + new Transition (9783, 9784), // &zdot -> ż + new Transition (9789, 9790), // &zeetrf -> ℨ + new Transition (9803, 9804), // &ZeroWidthSpace -> ​ + new Transition (9806, 9807), // &Zeta -> Ζ + new Transition (9809, 9810), // &zeta -> ζ + new Transition (9812, 9813), // &Zfr -> ℨ + new Transition (9815, 9816), // &zfr -> 𝔷 + new Transition (9819, 9820), // &ZHcy -> Ж + new Transition (9823, 9824), // &zhcy -> ж + new Transition (9830, 9831), // &zigrarr -> ⇝ + new Transition (9834, 9835), // &Zopf -> ℤ + new Transition (9838, 9839), // &zopf -> 𝕫 + new Transition (9842, 9843), // &Zscr -> 𝒵 + new Transition (9846, 9847), // &zscr -> 𝓏 + new Transition (9849, 9850), // &zwj -> ‍ + new Transition (9852, 9853) // &zwnj -> ‌ + }; + TransitionTable_A = new Transition[59] { + new Transition (0, 1), // & -> &A + new Transition (1432, 1447), // &d -> &dA + new Transition (1566, 1567), // &Diacritical -> &DiacriticalA + new Transition (1580, 1581), // &DiacriticalDouble -> &DiacriticalDoubleA + new Transition (1769, 1770), // &DoubleDown -> &DoubleDownA + new Transition (1779, 1780), // &DoubleLeft -> &DoubleLeftA + new Transition (1790, 1791), // &DoubleLeftRight -> &DoubleLeftRightA + new Transition (1807, 1808), // &DoubleLongLeft -> &DoubleLongLeftA + new Transition (1818, 1819), // &DoubleLongLeftRight -> &DoubleLongLeftRightA + new Transition (1829, 1830), // &DoubleLongRight -> &DoubleLongRightA + new Transition (1840, 1841), // &DoubleRight -> &DoubleRightA + new Transition (1852, 1853), // &DoubleUp -> &DoubleUpA + new Transition (1862, 1863), // &DoubleUpDown -> &DoubleUpDownA + new Transition (1882, 1883), // &Down -> &DownA + new Transition (1908, 1909), // &DownArrowUp -> &DownArrowUpA + new Transition (2015, 2017), // &DownTee -> &DownTeeA + new Transition (2616, 2617), // &For -> &ForA + new Transition (3014, 3035), // &H -> &HA + new Transition (3020, 3046), // &h -> &hA + new Transition (3692, 3693), // &l -> &lA + new Transition (3900, 3901), // &Left -> &LeftA + new Transition (3941, 3942), // &LeftArrowRight -> &LeftArrowRightA + new Transition (4034, 4035), // &LeftRight -> &LeftRightA + new Transition (4094, 4096), // &LeftTee -> &LeftTeeA + new Transition (4440, 4441), // &LongLeft -> &LongLeftA + new Transition (4473, 4474), // &LongLeftRight -> &LongLeftRightA + new Transition (4513, 4514), // &LongRight -> &LongRightA + new Transition (4594, 4595), // &LowerLeft -> &LowerLeftA + new Transition (4605, 4606), // &LowerRight -> &LowerRightA + new Transition (5064, 5071), // &ne -> &neA + new Transition (5227, 5228), // &nh -> &nhA + new Transition (5256, 5257), // &nl -> &nlA + new Transition (5855, 5856), // &nr -> &nrA + new Transition (6084, 6085), // &nvl -> &nvlA + new Transition (6097, 6098), // &nvr -> &nvrA + new Transition (6111, 6117), // &nw -> &nwA + new Transition (6876, 6877), // &r -> &rA + new Transition (7174, 7175), // &Right -> &RightA + new Transition (7216, 7217), // &RightArrowLeft -> &RightArrowLeftA + new Transition (7339, 7341), // &RightTee -> &RightTeeA + new Transition (7703, 7709), // &se -> &seA + new Transition (7779, 7780), // &ShortDown -> &ShortDownA + new Transition (7789, 7790), // &ShortLeft -> &ShortLeftA + new Transition (7816, 7817), // &ShortRight -> &ShortRightA + new Transition (7824, 7825), // &ShortUp -> &ShortUpA + new Transition (8375, 8381), // &sw -> &swA + new Transition (8623, 8624), // &TR -> &TRA + new Transition (8775, 8785), // &u -> &uA + new Transition (8970, 8971), // &Up -> &UpA + new Transition (8997, 8998), // &UpArrowDown -> &UpArrowDownA + new Transition (9007, 9008), // &UpDown -> &UpDownA + new Transition (9074, 9075), // &UpperLeft -> &UpperLeftA + new Transition (9085, 9086), // &UpperRight -> &UpperRightA + new Transition (9110, 9112), // &UpTee -> &UpTeeA + new Transition (9201, 9243), // &v -> &vA + new Transition (9572, 9573), // &xh -> &xhA + new Transition (9585, 9586), // &xl -> &xlA + new Transition (9623, 9624), // &xr -> &xrA + new Transition (9665, 9679) // &Y -> &YA + }; + TransitionTable_B = new Transition[34] { + new Transition (0, 331), // & -> &B + new Transition (1876, 1877), // &DoubleVertical -> &DoubleVerticalB + new Transition (1882, 1915), // &Down -> &DownB + new Transition (1887, 1903), // &DownArrow -> &DownArrowB + new Transition (1981, 1983), // &DownLeftVector -> &DownLeftVectorB + new Transition (2007, 2009), // &DownRightVector -> &DownRightVectorB + new Transition (3692, 3807), // &l -> &lB + new Transition (3905, 3906), // &LeftAngle -> &LeftAngleB + new Transition (3917, 3933), // &LeftArrow -> &LeftArrowB + new Transition (3966, 3967), // &LeftDouble -> &LeftDoubleB + new Transition (3992, 3994), // &LeftDownVector -> &LeftDownVectorB + new Transition (4126, 4128), // &LeftTriangle -> &LeftTriangleB + new Transition (4166, 4168), // &LeftUpVector -> &LeftUpVectorB + new Transition (4177, 4179), // &LeftVector -> &LeftVectorB + new Transition (5347, 5348), // &No -> &NoB + new Transition (5354, 5355), // &Non -> &NonB + new Transition (5409, 5410), // &NotDoubleVertical -> &NotDoubleVerticalB + new Transition (5539, 5541), // &NotLeftTriangle -> &NotLeftTriangleB + new Transition (5682, 5684), // &NotRightTriangle -> &NotRightTriangleB + new Transition (5816, 5817), // &NotVertical -> &NotVerticalB + new Transition (6437, 6438), // &Over -> &OverB + new Transition (6876, 6991), // &r -> &rB + new Transition (6886, 6986), // &R -> &RB + new Transition (7179, 7180), // &RightAngle -> &RightAngleB + new Transition (7191, 7209), // &RightArrow -> &RightArrowB + new Transition (7241, 7242), // &RightDouble -> &RightDoubleB + new Transition (7267, 7269), // &RightDownVector -> &RightDownVectorB + new Transition (7371, 7373), // &RightTriangle -> &RightTriangleB + new Transition (7411, 7413), // &RightUpVector -> &RightUpVectorB + new Transition (7422, 7424), // &RightVector -> &RightVectorB + new Transition (8919, 8920), // &Under -> &UnderB + new Transition (8975, 8990), // &UpArrow -> &UpArrowB + new Transition (9201, 9308), // &v -> &vB + new Transition (9377, 9378) // &Vertical -> &VerticalB + }; + TransitionTable_C = new Transition[15] { + new Transition (0, 789), // & -> &C + new Transition (1075, 1076), // &Clockwise -> &ClockwiseC + new Transition (1093, 1094), // &Close -> &CloseC + new Transition (1230, 1231), // &Counter -> &CounterC + new Transition (1239, 1240), // &CounterClockwise -> &CounterClockwiseC + new Transition (1316, 1326), // &Cup -> &CupC + new Transition (1747, 1748), // &Double -> &DoubleC + new Transition (3450, 3451), // &Invisible -> &InvisibleC + new Transition (3900, 3953), // &Left -> &LeftC + new Transition (5376, 5380), // &Not -> &NotC + new Transition (5391, 5392), // &NotCup -> &NotCupC + new Transition (6308, 6309), // &Open -> &OpenC + new Transition (7174, 7228), // &Right -> &RightC + new Transition (7756, 7757), // &SH -> &SHC + new Transition (7886, 7887) // &Small -> &SmallC + }; + TransitionTable_D = new Transition[43] { + new Transition (0, 1425), // & -> &D + new Transition (613, 618), // &box -> &boxD + new Transition (636, 640), // &boxH -> &boxHD + new Transition (638, 644), // &boxh -> &boxhD + new Transition (831, 832), // &Capital -> &CapitalD + new Transition (843, 844), // &CapitalDifferential -> &CapitalDifferentialD + new Transition (939, 940), // &Center -> &CenterD + new Transition (1023, 1024), // &Circle -> &CircleD + new Transition (1098, 1099), // &CloseCurly -> &CloseCurlyD + new Transition (1425, 1490), // &D -> &DD + new Transition (1566, 1573), // &Diacritical -> &DiacriticalD + new Transition (1630, 1631), // &Differential -> &DifferentialD + new Transition (1692, 1696), // &Dot -> &DotD + new Transition (1747, 1764), // &Double -> &DoubleD + new Transition (1852, 1859), // &DoubleUp -> &DoubleUpD + new Transition (2115, 2157), // &e -> &eD + new Transition (2157, 2158), // &eD -> &eDD + new Transition (2175, 2176), // &ef -> &efD + new Transition (2397, 2399), // &equiv -> &equivD + new Transition (2399, 2400), // &equivD -> &equivDD + new Transition (2409, 2414), // &er -> &erD + new Transition (3036, 3037), // &HAR -> &HARD + new Transition (3209, 3210), // &Hump -> &HumpD + new Transition (3900, 3961), // &Left -> &LeftD + new Transition (4139, 4140), // &LeftUp -> &LeftUpD + new Transition (4767, 4825), // &m -> &mD + new Transition (4825, 4826), // &mD -> &mDD + new Transition (5376, 5396), // &Not -> &NotD + new Transition (5496, 5497), // &NotHump -> &NotHumpD + new Transition (6043, 6058), // &nv -> &nvD + new Transition (6047, 6048), // &nV -> &nVD + new Transition (6313, 6314), // &OpenCurly -> &OpenCurlyD + new Transition (6488, 6489), // &Partial -> &PartialD + new Transition (7174, 7236), // &Right -> &RightD + new Transition (7384, 7385), // &RightUp -> &RightUpD + new Transition (7592, 7593), // &Rule -> &RuleD + new Transition (7775, 7776), // &Short -> &ShortD + new Transition (8624, 8625), // &TRA -> &TRAD + new Transition (8680, 8681), // &Triple -> &TripleD + new Transition (8970, 9004), // &Up -> &UpD + new Transition (8975, 8994), // &UpArrow -> &UpArrowD + new Transition (9201, 9330), // &v -> &vD + new Transition (9303, 9320) // &V -> &VD + }; + TransitionTable_E = new Transition[81] { + new Transition (0, 2108), // & -> &E + new Transition (1, 50), // &A -> &AE + new Transition (27, 31), // &ac -> &acE + new Transition (199, 206), // &ap -> &apE + new Transition (775, 777), // &bump -> &bumpE + new Transition (979, 1049), // &cir -> &cirE + new Transition (1692, 1707), // &Dot -> &DotE + new Transition (2490, 2491), // &Exponential -> &ExponentialE + new Transition (2701, 2763), // &g -> &gE + new Transition (2824, 2828), // &gl -> &glE + new Transition (2832, 2841), // &gn -> &gnE + new Transition (2871, 2872), // &Greater -> &GreaterE + new Transition (2886, 2887), // &GreaterFull -> &GreaterFullE + new Transition (2910, 2911), // &GreaterSlant -> &GreaterSlantE + new Transition (3011, 3012), // &gvn -> &gvnE + new Transition (3209, 3219), // &Hump -> &HumpE + new Transition (3236, 3269), // &I -> &IE + new Transition (3512, 3518), // &isin -> &isinE + new Transition (3692, 3894), // &l -> &lE + new Transition (4126, 4132), // &LeftTriangle -> &LeftTriangleE + new Transition (4239, 4240), // &Less -> &LessE + new Transition (4256, 4257), // &LessFull -> &LessFullE + new Transition (4288, 4289), // &LessSlant -> &LessSlantE + new Transition (4317, 4319), // &lg -> &lgE + new Transition (4401, 4410), // &ln -> &lnE + new Transition (4764, 4765), // &lvn -> &lvnE + new Transition (4986, 4988), // &nap -> &napE + new Transition (5195, 5196), // &ng -> &ngE + new Transition (5256, 5268), // &nl -> &nlE + new Transition (5376, 5414), // &Not -> &NotE + new Transition (5445, 5447), // &NotGreater -> &NotGreaterE + new Transition (5456, 5457), // &NotGreaterFull -> &NotGreaterFullE + new Transition (5480, 5481), // &NotGreaterSlant -> &NotGreaterSlantE + new Transition (5496, 5506), // &NotHump -> &NotHumpE + new Transition (5513, 5519), // ¬in -> ¬inE + new Transition (5539, 5545), // &NotLeftTriangle -> &NotLeftTriangleE + new Transition (5552, 5554), // &NotLess -> &NotLessE + new Transition (5577, 5578), // &NotLessSlant -> &NotLessSlantE + new Transition (5637, 5639), // &NotPrecedes -> &NotPrecedesE + new Transition (5649, 5650), // &NotPrecedesSlant -> &NotPrecedesSlantE + new Transition (5662, 5663), // &NotReverse -> &NotReverseE + new Transition (5682, 5688), // &NotRightTriangle -> &NotRightTriangleE + new Transition (5705, 5707), // &NotSquareSubset -> &NotSquareSubsetE + new Transition (5718, 5720), // &NotSquareSuperset -> &NotSquareSupersetE + new Transition (5730, 5732), // &NotSubset -> &NotSubsetE + new Transition (5743, 5745), // &NotSucceeds -> &NotSucceedsE + new Transition (5755, 5756), // &NotSucceedsSlant -> &NotSucceedsSlantE + new Transition (5773, 5775), // &NotSuperset -> &NotSupersetE + new Transition (5785, 5787), // &NotTilde -> &NotTildeE + new Transition (5796, 5797), // &NotTildeFull -> &NotTildeFullE + new Transition (5952, 5954), // &nsub -> &nsubE + new Transition (5973, 5975), // &nsup -> &nsupE + new Transition (6131, 6190), // &O -> &OE + new Transition (6642, 6651), // &pr -> &prE + new Transition (6677, 6679), // &Precedes -> &PrecedesE + new Transition (6689, 6690), // &PrecedesSlant -> &PrecedesSlantE + new Transition (6735, 6739), // &prn -> &prnE + new Transition (6886, 7092), // &R -> &RE + new Transition (7101, 7102), // &Reverse -> &ReverseE + new Transition (7122, 7123), // &ReverseUp -> &ReverseUpE + new Transition (7371, 7377), // &RightTriangle -> &RightTriangleE + new Transition (7631, 7649), // &sc -> &scE + new Transition (7670, 7674), // &scn -> &scnE + new Transition (7857, 7859), // &simg -> &simgE + new Transition (7861, 7863), // &siml -> &simlE + new Transition (8037, 8039), // &SquareSubset -> &SquareSubsetE + new Transition (8050, 8052), // &SquareSuperset -> &SquareSupersetE + new Transition (8131, 8137), // &sub -> &subE + new Transition (8150, 8151), // &subn -> &subnE + new Transition (8167, 8178), // &Subset -> &SubsetE + new Transition (8221, 8223), // &Succeeds -> &SucceedsE + new Transition (8233, 8234), // &SucceedsSlant -> &SucceedsSlantE + new Transition (8284, 8300), // &sup -> &supE + new Transition (8312, 8314), // &Superset -> &SupersetE + new Transition (8338, 8339), // &supn -> &supnE + new Transition (8547, 8554), // &Tilde -> &TildeE + new Transition (8563, 8564), // &TildeFull -> &TildeFullE + new Transition (8625, 8626), // &TRAD -> &TRADE + new Transition (8970, 9034), // &Up -> &UpE + new Transition (9460, 9461), // &vsubn -> &vsubnE + new Transition (9466, 9467) // &vsupn -> &vsupnE + }; + TransitionTable_F = new Transition[10] { + new Transition (0, 2517), // & -> &F + new Transition (219, 220), // &Apply -> &ApplyF + new Transition (2871, 2883), // &Greater -> &GreaterF + new Transition (3900, 3998), // &Left -> &LeftF + new Transition (4239, 4253), // &Less -> &LessF + new Transition (5445, 5453), // &NotGreater -> &NotGreaterF + new Transition (5785, 5793), // &NotTilde -> &NotTildeF + new Transition (7174, 7273), // &Right -> &RightF + new Transition (7930, 7931), // &SO -> &SOF + new Transition (8547, 8560) // &Tilde -> &TildeF + }; + TransitionTable_G = new Transition[15] { + new Transition (0, 2708), // & -> &G + new Transition (1566, 1587), // &Diacritical -> &DiacriticalG + new Transition (2287, 2288), // &EN -> &ENG + new Transition (2871, 2893), // &Greater -> &GreaterG + new Transition (4239, 4263), // &Less -> &LessG + new Transition (4244, 4245), // &LessEqual -> &LessEqualG + new Transition (4965, 5212), // &n -> &nG + new Transition (5151, 5152), // &Nested -> &NestedG + new Transition (5158, 5159), // &NestedGreater -> &NestedGreaterG + new Transition (5376, 5439), // &Not -> &NotG + new Transition (5445, 5463), // &NotGreater -> &NotGreaterG + new Transition (5552, 5560), // &NotLess -> &NotLessG + new Transition (5595, 5596), // &NotNested -> &NotNestedG + new Transition (5602, 5603), // &NotNestedGreater -> &NotNestedGreaterG + new Transition (7092, 7093) // &RE -> ® + }; + TransitionTable_H = new Transition[20] { + new Transition (0, 3014), // & -> &H + new Transition (613, 636), // &box -> &boxH + new Transition (691, 695), // &boxV -> &boxVH + new Transition (693, 699), // &boxv -> &boxvH + new Transition (789, 956), // &C -> &CH + new Transition (1432, 1546), // &d -> &dH + new Transition (2442, 2443), // &ET -> Ð + new Transition (3213, 3214), // &HumpDown -> &HumpDownH + new Transition (3618, 3660), // &K -> &KH + new Transition (3692, 4321), // &l -> &lH + new Transition (5376, 5493), // &Not -> &NotH + new Transition (5500, 5501), // &NotHumpDown -> &NotHumpDownH + new Transition (6043, 6073), // &nv -> &nvH + new Transition (6876, 7151), // &r -> &rH + new Transition (7610, 7756), // &S -> &SH + new Transition (7757, 7758), // &SHC -> &SHCH + new Transition (8400, 8535), // &T -> &TH + new Transition (8713, 8719), // &TS -> &TSH + new Transition (8775, 8872), // &u -> &uH + new Transition (9747, 9817) // &Z -> &ZH + }; + TransitionTable_I = new Transition[9] { + new Transition (0, 3236), // & -> &I + new Transition (1082, 1083), // &ClockwiseContour -> &ClockwiseContourI + new Transition (1190, 1191), // &Contour -> &ContourI + new Transition (1246, 1247), // &CounterClockwiseContour -> &CounterClockwiseContourI + new Transition (1754, 1755), // &DoubleContour -> &DoubleContourI + new Transition (3349, 3350), // &Imaginary -> &ImaginaryI + new Transition (7503, 7504), // &Round -> &RoundI + new Transition (8013, 8019), // &Square -> &SquareI + new Transition (9665, 9708) // &Y -> &YI + }; + TransitionTable_J = new Transition[7] { + new Transition (0, 3555), // & -> &J + new Transition (1425, 1661), // &D -> &DJ + new Transition (2708, 2816), // &G -> &GJ + new Transition (3236, 3320), // &I -> &IJ + new Transition (3618, 3668), // &K -> &KJ + new Transition (3698, 4338), // &L -> &LJ + new Transition (4971, 5248) // &N -> &NJ + }; + TransitionTable_K = new Transition[1] { + new Transition (0, 3618) // & -> &K + }; + TransitionTable_L = new Transition[29] { + new Transition (0, 3698), // & -> &L + new Transition (618, 619), // &boxD -> &boxDL + new Transition (623, 624), // &boxd -> &boxdL + new Transition (673, 674), // &boxU -> &boxUL + new Transition (678, 679), // &boxu -> &boxuL + new Transition (691, 703), // &boxV -> &boxVL + new Transition (693, 707), // &boxv -> &boxvL + new Transition (1747, 1776), // &Double -> &DoubleL + new Transition (1803, 1804), // &DoubleLong -> &DoubleLongL + new Transition (1882, 1950), // &Down -> &DownL + new Transition (2871, 2901), // &Greater -> &GreaterL + new Transition (2876, 2878), // &GreaterEqual -> &GreaterEqualL + new Transition (3178, 3179), // &Horizontal -> &HorizontalL + new Transition (4239, 4275), // &Less -> &LessL + new Transition (4436, 4437), // &Long -> &LongL + new Transition (4590, 4591), // &Lower -> &LowerL + new Transition (4965, 5272), // &n -> &nL + new Transition (5151, 5167), // &Nested -> &NestedL + new Transition (5170, 5171), // &NestedLess -> &NestedLessL + new Transition (5176, 5177), // &New -> &NewL + new Transition (5376, 5528), // &Not -> &NotL + new Transition (5445, 5471), // &NotGreater -> &NotGreaterL + new Transition (5552, 5568), // &NotLess -> &NotLessL + new Transition (5595, 5611), // &NotNested -> &NotNestedL + new Transition (5614, 5615), // &NotNestedLess -> &NotNestedLessL + new Transition (7191, 7213), // &RightArrow -> &RightArrowL + new Transition (7775, 7786), // &Short -> &ShortL + new Transition (9070, 9071), // &Upper -> &UpperL + new Transition (9377, 9382) // &Vertical -> &VerticalL + }; + TransitionTable_M = new Transition[5] { + new Transition (0, 4781), // & -> &M + new Transition (1, 111), // &A -> &AM + new Transition (1023, 1032), // &Circle -> &CircleM + new Transition (5090, 5091), // &Negative -> &NegativeM + new Transition (6589, 6590) // &Plus -> &PlusM + }; + TransitionTable_N = new Transition[5] { + new Transition (0, 4971), // & -> &N + new Transition (301, 587), // &b -> &bN + new Transition (2108, 2287), // &E -> &EN + new Transition (5376, 5590), // &Not -> &NotN + new Transition (8537, 8538) // &THOR -> Þ + }; + TransitionTable_O = new Transition[6] { + new Transition (0, 6131), // & -> &O + new Transition (789, 1217), // &C -> &CO + new Transition (3236, 3463), // &I -> &IO + new Transition (6869, 6870), // &QU -> &QUO + new Transition (7610, 7930), // &S -> &SO + new Transition (8535, 8536) // &TH -> &THO + }; + TransitionTable_P = new Transition[11] { + new Transition (0, 6482), // & -> &P + new Transition (111, 112), // &AM -> & + new Transition (1023, 1038), // &Circle -> &CircleP + new Transition (1217, 1218), // &CO -> &COP + new Transition (2954, 2955), // >l -> >lP + new Transition (4731, 4738), // <r -> <rP + new Transition (4903, 4904), // &Minus -> &MinusP + new Transition (5376, 5630), // &Not -> &NotP + new Transition (6437, 6451), // &Over -> &OverP + new Transition (8919, 8933), // &Under -> &UnderP + new Transition (8947, 8949) // &Union -> &UnionP + }; + TransitionTable_Q = new Transition[5] { + new Transition (0, 6813), // & -> &Q + new Transition (1098, 1111), // &CloseCurly -> &CloseCurlyQ + new Transition (1104, 1105), // &CloseCurlyDouble -> &CloseCurlyDoubleQ + new Transition (6313, 6326), // &OpenCurly -> &OpenCurlyQ + new Transition (6319, 6320) // &OpenCurlyDouble -> &OpenCurlyDoubleQ + }; + TransitionTable_R = new Transition[26] { + new Transition (0, 6886), // & -> &R + new Transition (618, 628), // &boxD -> &boxDR + new Transition (623, 632), // &boxd -> &boxdR + new Transition (673, 683), // &boxU -> &boxUR + new Transition (678, 687), // &boxu -> &boxuR + new Transition (691, 711), // &boxV -> &boxVR + new Transition (693, 715), // &boxv -> &boxvR + new Transition (1004, 1028), // &circled -> &circledR + new Transition (1747, 1836), // &Double -> &DoubleR + new Transition (1779, 1786), // &DoubleLeft -> &DoubleLeftR + new Transition (1803, 1825), // &DoubleLong -> &DoubleLongR + new Transition (1807, 1814), // &DoubleLongLeft -> &DoubleLongLeftR + new Transition (1882, 1987), // &Down -> &DownR + new Transition (1953, 1954), // &DownLeft -> &DownLeftR + new Transition (3035, 3036), // &HA -> &HAR + new Transition (3900, 4030), // &Left -> &LeftR + new Transition (3917, 3937), // &LeftArrow -> &LeftArrowR + new Transition (4436, 4509), // &Long -> &LongR + new Transition (4440, 4469), // &LongLeft -> &LongLeftR + new Transition (4590, 4601), // &Lower -> &LowerR + new Transition (4965, 5868), // &n -> &nR + new Transition (5376, 5656), // &Not -> &NotR + new Transition (7775, 7812), // &Short -> &ShortR + new Transition (8400, 8623), // &T -> &TR + new Transition (8536, 8537), // &THO -> &THOR + new Transition (9070, 9081) // &Upper -> &UpperR + }; + TransitionTable_S = new Transition[36] { + new Transition (0, 7610), // & -> &S + new Transition (1004, 1030), // &circled -> &circledS + new Transition (1425, 2048), // &D -> &DS + new Transition (2248, 2249), // &Empty -> &EmptyS + new Transition (2253, 2254), // &EmptySmall -> &EmptySmallS + new Transition (2266, 2267), // &EmptyVery -> &EmptyVeryS + new Transition (2271, 2272), // &EmptyVerySmall -> &EmptyVerySmallS + new Transition (2558, 2559), // &Filled -> &FilledS + new Transition (2563, 2564), // &FilledSmall -> &FilledSmallS + new Transition (2574, 2575), // &FilledVery -> &FilledVeryS + new Transition (2579, 2580), // &FilledVerySmall -> &FilledVerySmallS + new Transition (2871, 2906), // &Greater -> &GreaterS + new Transition (3105, 3106), // &Hilbert -> &HilbertS + new Transition (4239, 4284), // &Less -> &LessS + new Transition (4847, 4848), // &Medium -> &MediumS + new Transition (5096, 5097), // &NegativeMedium -> &NegativeMediumS + new Transition (5107, 5108), // &NegativeThick -> &NegativeThickS + new Transition (5114, 5115), // &NegativeThin -> &NegativeThinS + new Transition (5128, 5129), // &NegativeVeryThin -> &NegativeVeryThinS + new Transition (5362, 5363), // &NonBreaking -> &NonBreakingS + new Transition (5376, 5694), // &Not -> &NotS + new Transition (5445, 5476), // &NotGreater -> &NotGreaterS + new Transition (5552, 5573), // &NotLess -> &NotLessS + new Transition (5637, 5645), // &NotPrecedes -> &NotPrecedesS + new Transition (5699, 5700), // &NotSquare -> &NotSquareS + new Transition (5743, 5751), // &NotSucceeds -> &NotSucceedsS + new Transition (6138, 6376), // &o -> &oS + new Transition (6677, 6685), // &Precedes -> &PrecedesS + new Transition (8013, 8032), // &Square -> &SquareS + new Transition (8221, 8229), // &Succeeds -> &SucceedsS + new Transition (8400, 8713), // &T -> &TS + new Transition (8509, 8510), // &Thick -> &ThickS + new Transition (8520, 8521), // &Thin -> &ThinS + new Transition (9377, 9387), // &Vertical -> &VerticalS + new Transition (9407, 9408), // &VeryThin -> &VeryThinS + new Transition (9798, 9799) // &ZeroWidth -> &ZeroWidthS + }; + TransitionTable_T = new Transition[40] { + new Transition (0, 8400), // & -> &T + new Transition (1023, 1043), // &Circle -> &CircleT + new Transition (1566, 1593), // &Diacritical -> &DiacriticalT + new Transition (1779, 1797), // &DoubleLeft -> &DoubleLeftT + new Transition (1840, 1847), // &DoubleRight -> &DoubleRightT + new Transition (1882, 2013), // &Down -> &DownT + new Transition (1953, 1966), // &DownLeft -> &DownLeftT + new Transition (1991, 1992), // &DownRight -> &DownRightT + new Transition (2108, 2442), // &E -> &ET + new Transition (2370, 2377), // &Equal -> &EqualT + new Transition (2708, 2938), // &G -> > + new Transition (2871, 2917), // &Greater -> &GreaterT + new Transition (3450, 3457), // &Invisible -> &InvisibleT + new Transition (3698, 4694), // &L -> < + new Transition (3900, 4092), // &Left -> &LeftT + new Transition (3976, 3977), // &LeftDown -> &LeftDownT + new Transition (4139, 4151), // &LeftUp -> &LeftUpT + new Transition (4239, 4295), // &Less -> &LessT + new Transition (5090, 5103), // &Negative -> &NegativeT + new Transition (5124, 5125), // &NegativeVery -> &NegativeVeryT + new Transition (5376, 5781), // &Not -> &NotT + new Transition (5425, 5427), // &NotEqual -> &NotEqualT + new Transition (5445, 5487), // &NotGreater -> &NotGreaterT + new Transition (5531, 5532), // &NotLeft -> &NotLeftT + new Transition (5552, 5584), // &NotLess -> &NotLessT + new Transition (5674, 5675), // &NotRight -> &NotRightT + new Transition (5743, 5762), // &NotSucceeds -> &NotSucceedsT + new Transition (5785, 5803), // &NotTilde -> &NotTildeT + new Transition (6677, 6696), // &Precedes -> &PrecedesT + new Transition (6870, 6871), // &QUO -> " + new Transition (7174, 7337), // &Right -> &RightT + new Transition (7251, 7252), // &RightDown -> &RightDownT + new Transition (7384, 7396), // &RightUp -> &RightUpT + new Transition (7931, 7932), // &SOF -> &SOFT + new Transition (8221, 8240), // &Succeeds -> &SucceedsT + new Transition (8269, 8270), // &Such -> &SuchT + new Transition (8547, 8570), // &Tilde -> &TildeT + new Transition (8970, 9108), // &Up -> &UpT + new Transition (9377, 9397), // &Vertical -> &VerticalT + new Transition (9403, 9404) // &Very -> &VeryT + }; + TransitionTable_U = new Transition[13] { + new Transition (0, 8768), // & -> &U + new Transition (613, 673), // &box -> &boxU + new Transition (636, 648), // &boxH -> &boxHU + new Transition (638, 652), // &boxh -> &boxhU + new Transition (1747, 1851), // &Double -> &DoubleU + new Transition (1887, 1907), // &DownArrow -> &DownArrowU + new Transition (3900, 4138), // &Left -> &LeftU + new Transition (6813, 6869), // &Q -> &QU + new Transition (7101, 7121), // &Reverse -> &ReverseU + new Transition (7174, 7383), // &Right -> &RightU + new Transition (7775, 7823), // &Short -> &ShortU + new Transition (8013, 8058), // &Square -> &SquareU + new Transition (9665, 9732) // &Y -> &YU + }; + TransitionTable_V = new Transition[29] { + new Transition (0, 9303), // & -> &V + new Transition (613, 691), // &box -> &boxV + new Transition (1747, 1869), // &Double -> &DoubleV + new Transition (1953, 1976), // &DownLeft -> &DownLeftV + new Transition (1958, 1959), // &DownLeftRight -> &DownLeftRightV + new Transition (1968, 1969), // &DownLeftTee -> &DownLeftTeeV + new Transition (1991, 2002), // &DownRight -> &DownRightV + new Transition (1994, 1995), // &DownRightTee -> &DownRightTeeV + new Transition (2248, 2263), // &Empty -> &EmptyV + new Transition (2558, 2571), // &Filled -> &FilledV + new Transition (3900, 4172), // &Left -> &LeftV + new Transition (3976, 3987), // &LeftDown -> &LeftDownV + new Transition (3979, 3980), // &LeftDownTee -> &LeftDownTeeV + new Transition (4034, 4085), // &LeftRight -> &LeftRightV + new Transition (4094, 4102), // &LeftTee -> &LeftTeeV + new Transition (4139, 4161), // &LeftUp -> &LeftUpV + new Transition (4143, 4144), // &LeftUpDown -> &LeftUpDownV + new Transition (4153, 4154), // &LeftUpTee -> &LeftUpTeeV + new Transition (4965, 6047), // &n -> &nV + new Transition (5090, 5121), // &Negative -> &NegativeV + new Transition (5376, 5809), // &Not -> &NotV + new Transition (5401, 5402), // &NotDouble -> &NotDoubleV + new Transition (7174, 7417), // &Right -> &RightV + new Transition (7251, 7262), // &RightDown -> &RightDownV + new Transition (7254, 7255), // &RightDownTee -> &RightDownTeeV + new Transition (7339, 7347), // &RightTee -> &RightTeeV + new Transition (7384, 7406), // &RightUp -> &RightUpV + new Transition (7388, 7389), // &RightUpDown -> &RightUpDownV + new Transition (7398, 7399) // &RightUpTee -> &RightUpTeeV + }; + TransitionTable_W = new Transition[2] { + new Transition (0, 9484), // & -> &W + new Transition (9793, 9794) // &Zero -> &ZeroW + }; + TransitionTable_X = new Transition[1] { + new Transition (0, 9565) // & -> &X + }; + TransitionTable_Y = new Transition[2] { + new Transition (0, 9665), // & -> &Y + new Transition (1218, 1219) // &COP -> © + }; + TransitionTable_Z = new Transition[2] { + new Transition (0, 9747), // & -> &Z + new Transition (1425, 2093) // &D -> &DZ + }; + TransitionTable_a = new Transition[555] { + new Transition (0, 8), // & -> &a + new Transition (1, 2), // &A -> &Aa + new Transition (8, 9), // &a -> &aa + new Transition (68, 69), // &Agr -> &Agra + new Transition (74, 75), // &agr -> &agra + new Transition (91, 92), // &Alph -> &Alpha + new Transition (95, 96), // &alph -> &alpha + new Transition (98, 99), // &Am -> &Ama + new Transition (103, 104), // &am -> &ama + new Transition (120, 122), // &and -> &anda + new Transition (145, 147), // &angmsd -> &angmsda + new Transition (147, 148), // &angmsda -> &angmsdaa + new Transition (178, 179), // &angz -> &angza + new Transition (199, 201), // &ap -> &apa + new Transition (301, 302), // &b -> &ba + new Transition (331, 332), // &B -> &Ba + new Transition (336, 337), // &Backsl -> &Backsla + new Transition (385, 386), // &bec -> &beca + new Transition (391, 392), // &Bec -> &Beca + new Transition (423, 424), // &Bet -> &Beta + new Transition (426, 427), // &bet -> &beta + new Transition (444, 445), // &bigc -> &bigca + new Transition (477, 478), // &bigst -> &bigsta + new Transition (483, 484), // &bigtri -> &bigtria + new Transition (513, 514), // &bk -> &bka + new Transition (519, 520), // &bl -> &bla + new Transition (533, 534), // &blacksqu -> &blacksqua + new Transition (540, 541), // &blacktri -> &blacktria + new Transition (736, 737), // &brvb -> &brvba + new Transition (789, 790), // &C -> &Ca + new Transition (796, 797), // &c -> &ca + new Transition (805, 807), // &cap -> &capa + new Transition (817, 818), // &capc -> &capca + new Transition (829, 830), // &Capit -> &Capita + new Transition (841, 842), // &CapitalDifferenti -> &CapitalDifferentia + new Transition (861, 862), // &cc -> &cca + new Transition (866, 867), // &Cc -> &Cca + new Transition (924, 925), // &Cedill -> &Cedilla + new Transition (968, 969), // &checkm -> &checkma + new Transition (987, 988), // &circle -> &circlea + new Transition (1004, 1005), // &circled -> &circleda + new Transition (1014, 1015), // &circledd -> &circledda + new Transition (1088, 1089), // &ClockwiseContourIntegr -> &ClockwiseContourIntegra + new Transition (1143, 1144), // &comm -> &comma + new Transition (1196, 1197), // &ContourIntegr -> &ContourIntegra + new Transition (1252, 1253), // &CounterClockwiseContourIntegr -> &CounterClockwiseContourIntegra + new Transition (1256, 1257), // &cr -> &cra + new Transition (1293, 1294), // &cud -> &cuda + new Transition (1308, 1309), // &cul -> &cula + new Transition (1322, 1323), // &cupbrc -> &cupbrca + new Transition (1326, 1327), // &CupC -> &CupCa + new Transition (1330, 1331), // &cupc -> &cupca + new Transition (1346, 1347), // &cur -> &cura + new Transition (1382, 1383), // &curve -> &curvea + new Transition (1425, 1426), // &D -> &Da + new Transition (1432, 1433), // &d -> &da + new Transition (1464, 1465), // &dbk -> &dbka + new Transition (1470, 1471), // &dbl -> &dbla + new Transition (1474, 1475), // &Dc -> &Dca + new Transition (1480, 1481), // &dc -> &dca + new Transition (1492, 1494), // &dd -> &dda + new Transition (1505, 1506), // &DDotr -> &DDotra + new Transition (1522, 1523), // &Delt -> &Delta + new Transition (1526, 1527), // &delt -> &delta + new Transition (1546, 1547), // &dH -> &dHa + new Transition (1550, 1551), // &dh -> &dha + new Transition (1557, 1558), // &Di -> &Dia + new Transition (1564, 1565), // &Diacritic -> &Diacritica + new Transition (1588, 1589), // &DiacriticalGr -> &DiacriticalGra + new Transition (1599, 1600), // &di -> &dia + new Transition (1628, 1629), // &Differenti -> &Differentia + new Transition (1633, 1634), // &dig -> &diga + new Transition (1636, 1637), // &digamm -> &digamma + new Transition (1681, 1682), // &doll -> &dolla + new Transition (1709, 1710), // &DotEqu -> &DotEqua + new Transition (1726, 1727), // &dotsqu -> &dotsqua + new Transition (1735, 1736), // &doubleb -> &doubleba + new Transition (1760, 1761), // &DoubleContourIntegr -> &DoubleContourIntegra + new Transition (1874, 1875), // &DoubleVertic -> &DoubleVertica + new Transition (1877, 1878), // &DoubleVerticalB -> &DoubleVerticalBa + new Transition (1882, 1889), // &Down -> &Downa + new Transition (1896, 1897), // &down -> &downa + new Transition (1903, 1904), // &DownArrowB -> &DownArrowBa + new Transition (1924, 1925), // &downdown -> &downdowna + new Transition (1932, 1933), // &downh -> &downha + new Transition (1983, 1984), // &DownLeftVectorB -> &DownLeftVectorBa + new Transition (2009, 2010), // &DownRightVectorB -> &DownRightVectorBa + new Transition (2025, 2026), // &drbk -> &drbka + new Transition (2077, 2078), // &du -> &dua + new Transition (2082, 2083), // &duh -> &duha + new Transition (2086, 2087), // &dw -> &dwa + new Transition (2103, 2104), // &dzigr -> &dzigra + new Transition (2108, 2109), // &E -> &Ea + new Transition (2115, 2116), // &e -> &ea + new Transition (2127, 2128), // &Ec -> &Eca + new Transition (2133, 2134), // &ec -> &eca + new Transition (2188, 2189), // &Egr -> &Egra + new Transition (2193, 2194), // &egr -> &egra + new Transition (2228, 2229), // &Em -> &Ema + new Transition (2233, 2234), // &em -> &ema + new Transition (2250, 2251), // &EmptySm -> &EmptySma + new Transition (2256, 2257), // &EmptySmallSqu -> &EmptySmallSqua + new Transition (2268, 2269), // &EmptyVerySm -> &EmptyVerySma + new Transition (2274, 2275), // &EmptyVerySmallSqu -> &EmptyVerySmallSqua + new Transition (2312, 2313), // &ep -> &epa + new Transition (2354, 2355), // &eqsl -> &eqsla + new Transition (2368, 2369), // &Equ -> &Equa + new Transition (2372, 2373), // &equ -> &equa + new Transition (2403, 2404), // &eqvp -> &eqvpa + new Transition (2409, 2410), // &er -> &era + new Transition (2436, 2437), // &Et -> &Eta + new Transition (2439, 2440), // &et -> &eta + new Transition (2475, 2476), // &expect -> &expecta + new Transition (2488, 2489), // &Exponenti -> &Exponentia + new Transition (2498, 2499), // &exponenti -> &exponentia + new Transition (2503, 2504), // &f -> &fa + new Transition (2525, 2526), // &fem -> &fema + new Transition (2560, 2561), // &FilledSm -> &FilledSma + new Transition (2566, 2567), // &FilledSmallSqu -> &FilledSmallSqua + new Transition (2576, 2577), // &FilledVerySm -> &FilledVerySma + new Transition (2582, 2583), // &FilledVerySmallSqu -> &FilledVerySmallSqua + new Transition (2592, 2593), // &fl -> &fla + new Transition (2621, 2622), // &for -> &fora + new Transition (2639, 2640), // &fp -> &fpa + new Transition (2647, 2648), // &fr -> &fra + new Transition (2701, 2702), // &g -> &ga + new Transition (2708, 2709), // &G -> &Ga + new Transition (2711, 2712), // &Gamm -> &Gamma + new Transition (2715, 2716), // &gamm -> &gamma + new Transition (2776, 2777), // &geqsl -> &geqsla + new Transition (2824, 2826), // &gl -> &gla + new Transition (2832, 2833), // &gn -> &gna + new Transition (2861, 2862), // &gr -> &gra + new Transition (2867, 2868), // &Gre -> &Grea + new Transition (2874, 2875), // &GreaterEqu -> &GreaterEqua + new Transition (2889, 2890), // &GreaterFullEqu -> &GreaterFullEqua + new Transition (2895, 2896), // &GreaterGre -> &GreaterGrea + new Transition (2907, 2908), // &GreaterSl -> &GreaterSla + new Transition (2913, 2914), // &GreaterSlantEqu -> &GreaterSlantEqua + new Transition (2955, 2956), // >lP -> >lPa + new Transition (2965, 2966), // >r -> >ra + new Transition (3014, 3015), // &H -> &Ha + new Transition (3020, 3021), // &h -> &ha + new Transition (3060, 3061), // &hb -> &hba + new Transition (3074, 3075), // &he -> &hea + new Transition (3107, 3108), // &HilbertSp -> &HilbertSpa + new Transition (3114, 3115), // &hkse -> &hksea + new Transition (3120, 3121), // &hksw -> &hkswa + new Transition (3126, 3127), // &ho -> &hoa + new Transition (3141, 3142), // &hookleft -> &hooklefta + new Transition (3152, 3153), // &hookright -> &hookrighta + new Transition (3167, 3168), // &horb -> &horba + new Transition (3176, 3177), // &Horizont -> &Horizonta + new Transition (3192, 3193), // &hsl -> &hsla + new Transition (3221, 3222), // &HumpEqu -> &HumpEqua + new Transition (3236, 3237), // &I -> &Ia + new Transition (3243, 3244), // &i -> &ia + new Transition (3290, 3291), // &Igr -> &Igra + new Transition (3296, 3297), // &igr -> &igra + new Transition (3317, 3318), // &iiot -> &iiota + new Transition (3330, 3332), // &Im -> &Ima + new Transition (3336, 3337), // &im -> &ima + new Transition (3346, 3347), // &Imagin -> &Imagina + new Transition (3357, 3358), // &imagp -> &imagpa + new Transition (3380, 3381), // &inc -> &inca + new Transition (3403, 3404), // &intc -> &intca + new Transition (3415, 3416), // &Integr -> &Integra + new Transition (3420, 3421), // &interc -> &interca + new Transition (3433, 3434), // &intl -> &intla + new Transition (3454, 3455), // &InvisibleComm -> &InvisibleComma + new Transition (3486, 3487), // &Iot -> &Iota + new Transition (3489, 3490), // &iot -> &iota + new Transition (3577, 3578), // &jm -> &jma + new Transition (3618, 3619), // &K -> &Ka + new Transition (3621, 3622), // &Kapp -> &Kappa + new Transition (3624, 3625), // &k -> &ka + new Transition (3627, 3628), // &kapp -> &kappa + new Transition (3692, 3705), // &l -> &la + new Transition (3693, 3694), // &lA -> &lAa + new Transition (3698, 3699), // &L -> &La + new Transition (3719, 3720), // &lagr -> &lagra + new Transition (3725, 3726), // &Lambd -> &Lambda + new Transition (3730, 3731), // &lambd -> &lambda + new Transition (3747, 3748), // &Lapl -> &Lapla + new Transition (3792, 3799), // &lat -> &lata + new Transition (3794, 3795), // &lAt -> &lAta + new Transition (3807, 3808), // &lB -> &lBa + new Transition (3812, 3813), // &lb -> &lba + new Transition (3821, 3822), // &lbr -> &lbra + new Transition (3837, 3838), // &Lc -> &Lca + new Transition (3843, 3844), // &lc -> &lca + new Transition (3870, 3871), // &ldc -> &ldca + new Transition (3881, 3882), // &ldrdh -> &ldrdha + new Transition (3887, 3888), // &ldrush -> &ldrusha + new Transition (3900, 3919), // &Left -> &Lefta + new Transition (3907, 3908), // &LeftAngleBr -> &LeftAngleBra + new Transition (3926, 3927), // &left -> &lefta + new Transition (3933, 3934), // &LeftArrowB -> &LeftArrowBa + new Transition (3948, 3949), // &leftarrowt -> &leftarrowta + new Transition (3968, 3969), // &LeftDoubleBr -> &LeftDoubleBra + new Transition (3994, 3995), // &LeftDownVectorB -> &LeftDownVectorBa + new Transition (4004, 4005), // &lefth -> &leftha + new Transition (4022, 4023), // &leftleft -> &leftlefta + new Transition (4045, 4046), // &Leftright -> &Leftrighta + new Transition (4056, 4057), // &leftright -> &leftrighta + new Transition (4065, 4066), // &leftrighth -> &leftrightha + new Transition (4078, 4079), // &leftrightsquig -> &leftrightsquiga + new Transition (4121, 4122), // &LeftTri -> &LeftTria + new Transition (4128, 4129), // &LeftTriangleB -> &LeftTriangleBa + new Transition (4134, 4135), // &LeftTriangleEqu -> &LeftTriangleEqua + new Transition (4168, 4169), // &LeftUpVectorB -> &LeftUpVectorBa + new Transition (4179, 4180), // &LeftVectorB -> &LeftVectorBa + new Transition (4192, 4193), // &leqsl -> &leqsla + new Transition (4215, 4216), // &less -> &lessa + new Transition (4242, 4243), // &LessEqu -> &LessEqua + new Transition (4247, 4248), // &LessEqualGre -> &LessEqualGrea + new Transition (4259, 4260), // &LessFullEqu -> &LessFullEqua + new Transition (4265, 4266), // &LessGre -> &LessGrea + new Transition (4285, 4286), // &LessSl -> &LessSla + new Transition (4291, 4292), // &LessSlantEqu -> &LessSlantEqua + new Transition (4321, 4322), // &lH -> &lHa + new Transition (4325, 4326), // &lh -> &lha + new Transition (4348, 4350), // &ll -> &lla + new Transition (4363, 4364), // &Lleft -> &Llefta + new Transition (4370, 4371), // &llh -> &llha + new Transition (4394, 4396), // &lmoust -> &lmousta + new Transition (4401, 4402), // &ln -> &lna + new Transition (4422, 4423), // &lo -> &loa + new Transition (4450, 4451), // &Longleft -> &Longlefta + new Transition (4462, 4463), // &longleft -> &longlefta + new Transition (4484, 4485), // &Longleftright -> &Longleftrighta + new Transition (4495, 4496), // &longleftright -> &longleftrighta + new Transition (4502, 4503), // &longm -> &longma + new Transition (4524, 4525), // &Longright -> &Longrighta + new Transition (4535, 4536), // &longright -> &longrighta + new Transition (4543, 4544), // &loop -> &loopa + new Transition (4560, 4561), // &lop -> &lopa + new Transition (4579, 4580), // &low -> &lowa + new Transition (4584, 4585), // &lowb -> &lowba + new Transition (4621, 4622), // &lp -> &lpa + new Transition (4628, 4629), // &lr -> &lra + new Transition (4640, 4641), // &lrh -> &lrha + new Transition (4652, 4653), // &ls -> &lsa + new Transition (4720, 4721), // <l -> <la + new Transition (4738, 4739), // <rP -> <rPa + new Transition (4746, 4747), // &lurdsh -> &lurdsha + new Transition (4751, 4752), // &luruh -> &luruha + new Transition (4767, 4768), // &m -> &ma + new Transition (4781, 4782), // &M -> &Ma + new Transition (4812, 4813), // &mcomm -> &mcomma + new Transition (4820, 4821), // &md -> &mda + new Transition (4830, 4831), // &me -> &mea + new Transition (4836, 4837), // &measured -> &measureda + new Transition (4849, 4850), // &MediumSp -> &MediumSpa + new Transition (4876, 4878), // &mid -> &mida + new Transition (4957, 4958), // &multim -> &multima + new Transition (4961, 4962), // &mum -> &muma + new Transition (4965, 4966), // &n -> &na + new Transition (4968, 4969), // &nabl -> &nabla + new Transition (4971, 4972), // &N -> &Na + new Transition (5003, 5005), // &natur -> &natura + new Transition (5020, 5021), // &nc -> &nca + new Transition (5024, 5025), // &Nc -> &Nca + new Transition (5059, 5060), // &nd -> &nda + new Transition (5064, 5066), // &ne -> &nea + new Transition (5085, 5086), // &Neg -> &Nega + new Transition (5098, 5099), // &NegativeMediumSp -> &NegativeMediumSpa + new Transition (5109, 5110), // &NegativeThickSp -> &NegativeThickSpa + new Transition (5116, 5117), // &NegativeThinSp -> &NegativeThinSpa + new Transition (5130, 5131), // &NegativeVeryThinSp -> &NegativeVeryThinSpa + new Transition (5141, 5142), // &nese -> &nesea + new Transition (5154, 5155), // &NestedGre -> &NestedGrea + new Transition (5161, 5162), // &NestedGreaterGre -> &NestedGreaterGrea + new Transition (5205, 5206), // &ngeqsl -> &ngeqsla + new Transition (5227, 5232), // &nh -> &nha + new Transition (5236, 5237), // &nhp -> &nhpa + new Transition (5256, 5261), // &nl -> &nla + new Transition (5275, 5276), // &nLeft -> &nLefta + new Transition (5283, 5284), // &nleft -> &nlefta + new Transition (5294, 5295), // &nLeftright -> &nLeftrighta + new Transition (5305, 5306), // &nleftright -> &nleftrighta + new Transition (5317, 5318), // &nleqsl -> &nleqsla + new Transition (5350, 5351), // &NoBre -> &NoBrea + new Transition (5357, 5358), // &NonBre -> &NonBrea + new Transition (5364, 5365), // &NonBreakingSp -> &NonBreakingSpa + new Transition (5392, 5393), // &NotCupC -> &NotCupCa + new Transition (5407, 5408), // &NotDoubleVertic -> &NotDoubleVertica + new Transition (5410, 5411), // &NotDoubleVerticalB -> &NotDoubleVerticalBa + new Transition (5423, 5424), // &NotEqu -> &NotEqua + new Transition (5441, 5442), // &NotGre -> &NotGrea + new Transition (5449, 5450), // &NotGreaterEqu -> &NotGreaterEqua + new Transition (5459, 5460), // &NotGreaterFullEqu -> &NotGreaterFullEqua + new Transition (5465, 5466), // &NotGreaterGre -> &NotGreaterGrea + new Transition (5477, 5478), // &NotGreaterSl -> &NotGreaterSla + new Transition (5483, 5484), // &NotGreaterSlantEqu -> &NotGreaterSlantEqua + new Transition (5508, 5509), // &NotHumpEqu -> &NotHumpEqua + new Transition (5521, 5522), // ¬inv -> ¬inva + new Transition (5534, 5535), // &NotLeftTri -> &NotLeftTria + new Transition (5541, 5542), // &NotLeftTriangleB -> &NotLeftTriangleBa + new Transition (5547, 5548), // &NotLeftTriangleEqu -> &NotLeftTriangleEqua + new Transition (5556, 5557), // &NotLessEqu -> &NotLessEqua + new Transition (5562, 5563), // &NotLessGre -> &NotLessGrea + new Transition (5574, 5575), // &NotLessSl -> &NotLessSla + new Transition (5580, 5581), // &NotLessSlantEqu -> &NotLessSlantEqua + new Transition (5598, 5599), // &NotNestedGre -> &NotNestedGrea + new Transition (5605, 5606), // &NotNestedGreaterGre -> &NotNestedGreaterGrea + new Transition (5623, 5624), // ¬niv -> ¬niva + new Transition (5641, 5642), // &NotPrecedesEqu -> &NotPrecedesEqua + new Transition (5646, 5647), // &NotPrecedesSl -> &NotPrecedesSla + new Transition (5652, 5653), // &NotPrecedesSlantEqu -> &NotPrecedesSlantEqua + new Transition (5677, 5678), // &NotRightTri -> &NotRightTria + new Transition (5684, 5685), // &NotRightTriangleB -> &NotRightTriangleBa + new Transition (5690, 5691), // &NotRightTriangleEqu -> &NotRightTriangleEqua + new Transition (5696, 5697), // &NotSqu -> &NotSqua + new Transition (5709, 5710), // &NotSquareSubsetEqu -> &NotSquareSubsetEqua + new Transition (5722, 5723), // &NotSquareSupersetEqu -> &NotSquareSupersetEqua + new Transition (5734, 5735), // &NotSubsetEqu -> &NotSubsetEqua + new Transition (5747, 5748), // &NotSucceedsEqu -> &NotSucceedsEqua + new Transition (5752, 5753), // &NotSucceedsSl -> &NotSucceedsSla + new Transition (5758, 5759), // &NotSucceedsSlantEqu -> &NotSucceedsSlantEqua + new Transition (5777, 5778), // &NotSupersetEqu -> &NotSupersetEqua + new Transition (5789, 5790), // &NotTildeEqu -> &NotTildeEqua + new Transition (5799, 5800), // &NotTildeFullEqu -> &NotTildeFullEqua + new Transition (5814, 5815), // &NotVertic -> &NotVertica + new Transition (5817, 5818), // &NotVerticalB -> &NotVerticalBa + new Transition (5821, 5822), // &np -> &npa + new Transition (5823, 5825), // &npar -> &npara + new Transition (5855, 5860), // &nr -> &nra + new Transition (5872, 5873), // &nRight -> &nRighta + new Transition (5882, 5883), // &nright -> &nrighta + new Transition (5918, 5919), // &nshortp -> &nshortpa + new Transition (5920, 5921), // &nshortpar -> &nshortpara + new Transition (5938, 5939), // &nsp -> &nspa + new Transition (6007, 6008), // &ntri -> &ntria + new Transition (6043, 6044), // &nv -> &nva + new Transition (6048, 6049), // &nVD -> &nVDa + new Transition (6053, 6054), // &nVd -> &nVda + new Transition (6058, 6059), // &nvD -> &nvDa + new Transition (6063, 6064), // &nvd -> &nvda + new Transition (6073, 6074), // &nvH -> &nvHa + new Transition (6111, 6112), // &nw -> &nwa + new Transition (6127, 6128), // &nwne -> &nwnea + new Transition (6131, 6132), // &O -> &Oa + new Transition (6138, 6139), // &o -> &oa + new Transition (6163, 6164), // &od -> &oda + new Transition (6170, 6171), // &Odbl -> &Odbla + new Transition (6175, 6176), // &odbl -> &odbla + new Transition (6215, 6216), // &Ogr -> &Ogra + new Transition (6220, 6221), // &ogr -> &ogra + new Transition (6228, 6229), // &ohb -> &ohba + new Transition (6238, 6239), // &ol -> &ola + new Transition (6258, 6259), // &Om -> &Oma + new Transition (6263, 6264), // &om -> &oma + new Transition (6269, 6270), // &Omeg -> &Omega + new Transition (6273, 6274), // &omeg -> &omega + new Transition (6302, 6303), // &op -> &opa + new Transition (6342, 6344), // &or -> &ora + new Transition (6386, 6387), // &Osl -> &Osla + new Transition (6391, 6392), // &osl -> &osla + new Transition (6417, 6419), // &otimes -> &otimesa + new Transition (6431, 6432), // &ovb -> &ovba + new Transition (6438, 6439), // &OverB -> &OverBa + new Transition (6442, 6443), // &OverBr -> &OverBra + new Transition (6451, 6452), // &OverP -> &OverPa + new Transition (6463, 6464), // &p -> &pa + new Transition (6465, 6467), // &par -> ¶ + new Transition (6482, 6483), // &P -> &Pa + new Transition (6486, 6487), // &Parti -> &Partia + new Transition (6533, 6534), // &phmm -> &phmma + new Transition (6555, 6556), // &pl -> &pla + new Transition (6567, 6569), // &plus -> &plusa + new Transition (6612, 6613), // &Poinc -> &Poinca + new Transition (6617, 6618), // &Poincarepl -> &Poincarepla + new Transition (6642, 6644), // &pr -> &pra + new Transition (6655, 6657), // &prec -> &preca + new Transition (6681, 6682), // &PrecedesEqu -> &PrecedesEqua + new Transition (6686, 6687), // &PrecedesSl -> &PrecedesSla + new Transition (6692, 6693), // &PrecedesSlantEqu -> &PrecedesSlantEqua + new Transition (6705, 6706), // &precn -> &precna + new Transition (6735, 6736), // &prn -> &prna + new Transition (6754, 6755), // &prof -> &profa + new Transition (6756, 6757), // &profal -> &profala + new Transition (6778, 6780), // &Proportion -> &Proportiona + new Transition (6847, 6848), // &qu -> &qua + new Transition (6876, 6882), // &r -> &ra + new Transition (6877, 6878), // &rA -> &rAa + new Transition (6886, 6887), // &R -> &Ra + new Transition (6932, 6934), // &rarr -> &rarra + new Transition (6968, 6969), // &rAt -> &rAta + new Transition (6973, 6974), // &rat -> &rata + new Transition (6981, 6982), // &ration -> &rationa + new Transition (6986, 6987), // &RB -> &RBa + new Transition (6991, 6992), // &rB -> &rBa + new Transition (6996, 6997), // &rb -> &rba + new Transition (7005, 7006), // &rbr -> &rbra + new Transition (7021, 7022), // &Rc -> &Rca + new Transition (7027, 7028), // &rc -> &rca + new Transition (7054, 7055), // &rdc -> &rdca + new Transition (7059, 7060), // &rdldh -> &rdldha + new Transition (7074, 7075), // &re -> &rea + new Transition (7082, 7083), // &realp -> &realpa + new Transition (7151, 7152), // &rH -> &rHa + new Transition (7155, 7156), // &rh -> &rha + new Transition (7174, 7193), // &Right -> &Righta + new Transition (7181, 7182), // &RightAngleBr -> &RightAngleBra + new Transition (7202, 7203), // &right -> &righta + new Transition (7209, 7210), // &RightArrowB -> &RightArrowBa + new Transition (7223, 7224), // &rightarrowt -> &rightarrowta + new Transition (7243, 7244), // &RightDoubleBr -> &RightDoubleBra + new Transition (7269, 7270), // &RightDownVectorB -> &RightDownVectorBa + new Transition (7279, 7280), // &righth -> &rightha + new Transition (7297, 7298), // &rightleft -> &rightlefta + new Transition (7305, 7306), // &rightlefth -> &rightleftha + new Transition (7318, 7319), // &rightright -> &rightrighta + new Transition (7330, 7331), // &rightsquig -> &rightsquiga + new Transition (7366, 7367), // &RightTri -> &RightTria + new Transition (7373, 7374), // &RightTriangleB -> &RightTriangleBa + new Transition (7379, 7380), // &RightTriangleEqu -> &RightTriangleEqua + new Transition (7413, 7414), // &RightUpVectorB -> &RightUpVectorBa + new Transition (7424, 7425), // &RightVectorB -> &RightVectorBa + new Transition (7442, 7443), // &rl -> &rla + new Transition (7447, 7448), // &rlh -> &rlha + new Transition (7457, 7459), // &rmoust -> &rmousta + new Transition (7469, 7470), // &ro -> &roa + new Transition (7481, 7482), // &rop -> &ropa + new Transition (7512, 7513), // &rp -> &rpa + new Transition (7526, 7527), // &rr -> &rra + new Transition (7535, 7536), // &Rright -> &Rrighta + new Transition (7542, 7543), // &rs -> &rsa + new Transition (7595, 7596), // &RuleDel -> &RuleDela + new Transition (7604, 7605), // &ruluh -> &ruluha + new Transition (7610, 7611), // &S -> &Sa + new Transition (7617, 7618), // &s -> &sa + new Transition (7629, 7636), // &Sc -> &Sca + new Transition (7631, 7633), // &sc -> &sca + new Transition (7670, 7671), // &scn -> &scna + new Transition (7703, 7704), // &se -> &sea + new Transition (7725, 7726), // &sesw -> &seswa + new Transition (7751, 7752), // &sh -> &sha + new Transition (7803, 7804), // &shortp -> &shortpa + new Transition (7805, 7806), // &shortpar -> &shortpara + new Transition (7835, 7836), // &Sigm -> &Sigma + new Transition (7840, 7841), // &sigm -> &sigma + new Transition (7873, 7874), // &simr -> &simra + new Transition (7878, 7879), // &sl -> &sla + new Transition (7883, 7884), // &Sm -> &Sma + new Transition (7894, 7895), // &sm -> &sma + new Transition (7912, 7913), // &smep -> &smepa + new Transition (7944, 7946), // &solb -> &solba + new Transition (7956, 7957), // &sp -> &spa + new Transition (7969, 7970), // &sqc -> &sqca + new Transition (8008, 8015), // &squ -> &squa + new Transition (8010, 8011), // &Squ -> &Squa + new Transition (8041, 8042), // &SquareSubsetEqu -> &SquareSubsetEqua + new Transition (8054, 8055), // &SquareSupersetEqu -> &SquareSupersetEqua + new Transition (8068, 8069), // &sr -> &sra + new Transition (8091, 8092), // &sst -> &ssta + new Transition (8096, 8097), // &St -> &Sta + new Transition (8100, 8101), // &st -> &sta + new Transition (8106, 8107), // &str -> &stra + new Transition (8160, 8161), // &subr -> &subra + new Transition (8180, 8181), // &SubsetEqu -> &SubsetEqua + new Transition (8199, 8201), // &succ -> &succa + new Transition (8225, 8226), // &SucceedsEqu -> &SucceedsEqua + new Transition (8230, 8231), // &SucceedsSl -> &SucceedsSla + new Transition (8236, 8237), // &SucceedsSlantEqu -> &SucceedsSlantEqua + new Transition (8249, 8250), // &succn -> &succna + new Transition (8271, 8272), // &SuchTh -> &SuchTha + new Transition (8316, 8317), // &SupersetEqu -> &SupersetEqua + new Transition (8328, 8329), // &supl -> &supla + new Transition (8375, 8376), // &sw -> &swa + new Transition (8391, 8392), // &swnw -> &swnwa + new Transition (8400, 8401), // &T -> &Ta + new Transition (8404, 8405), // &t -> &ta + new Transition (8419, 8420), // &Tc -> &Tca + new Transition (8425, 8426), // &tc -> &tca + new Transition (8481, 8482), // &Thet -> &Theta + new Transition (8484, 8485), // &thet -> &theta + new Transition (8495, 8496), // &thick -> &thicka + new Transition (8511, 8512), // &ThickSp -> &ThickSpa + new Transition (8522, 8523), // &ThinSp -> &ThinSpa + new Transition (8527, 8528), // &thk -> &thka + new Transition (8556, 8557), // &TildeEqu -> &TildeEqua + new Transition (8566, 8567), // &TildeFullEqu -> &TildeFullEqua + new Transition (8580, 8582), // ×b -> ×ba + new Transition (8591, 8592), // &toe -> &toea + new Transition (8614, 8615), // &tos -> &tosa + new Transition (8628, 8629), // &tr -> &tra + new Transition (8633, 8634), // &tri -> &tria + new Transition (8744, 8745), // &twohe -> &twohea + new Transition (8750, 8751), // &twoheadleft -> &twoheadlefta + new Transition (8761, 8762), // &twoheadright -> &twoheadrighta + new Transition (8768, 8769), // &U -> &Ua + new Transition (8775, 8776), // &u -> &ua + new Transition (8829, 8830), // &ud -> &uda + new Transition (8836, 8837), // &Udbl -> &Udbla + new Transition (8841, 8842), // &udbl -> &udbla + new Transition (8845, 8846), // &udh -> &udha + new Transition (8861, 8862), // &Ugr -> &Ugra + new Transition (8867, 8868), // &ugr -> &ugra + new Transition (8872, 8873), // &uH -> &uHa + new Transition (8876, 8877), // &uh -> &uha + new Transition (8904, 8905), // &Um -> &Uma + new Transition (8909, 8910), // &um -> &uma + new Transition (8920, 8921), // &UnderB -> &UnderBa + new Transition (8924, 8925), // &UnderBr -> &UnderBra + new Transition (8933, 8934), // &UnderP -> &UnderPa + new Transition (8970, 8977), // &Up -> &Upa + new Transition (8983, 8984), // &up -> &upa + new Transition (8990, 8991), // &UpArrowB -> &UpArrowBa + new Transition (9017, 9018), // &Updown -> &Updowna + new Transition (9027, 9028), // &updown -> &updowna + new Transition (9046, 9047), // &uph -> &upha + new Transition (9119, 9120), // &upup -> &upupa + new Transition (9182, 9183), // &uu -> &uua + new Transition (9194, 9195), // &uw -> &uwa + new Transition (9201, 9202), // &v -> &va + new Transition (9217, 9218), // &vark -> &varka + new Transition (9220, 9221), // &varkapp -> &varkappa + new Transition (9255, 9256), // &varsigm -> &varsigma + new Transition (9282, 9283), // &varthet -> &vartheta + new Transition (9286, 9287), // &vartri -> &vartria + new Transition (9304, 9305), // &Vb -> &Vba + new Transition (9308, 9309), // &vB -> &vBa + new Transition (9320, 9321), // &VD -> &VDa + new Transition (9325, 9326), // &Vd -> &Vda + new Transition (9330, 9331), // &vD -> &vDa + new Transition (9335, 9336), // &vd -> &vda + new Transition (9348, 9349), // &veeb -> &veeba + new Transition (9361, 9362), // &Verb -> &Verba + new Transition (9366, 9367), // &verb -> &verba + new Transition (9375, 9376), // &Vertic -> &Vertica + new Transition (9378, 9379), // &VerticalB -> &VerticalBa + new Transition (9389, 9390), // &VerticalSep -> &VerticalSepa + new Transition (9391, 9392), // &VerticalSepar -> &VerticalSepara + new Transition (9409, 9410), // &VeryThinSp -> &VeryThinSpa + new Transition (9472, 9473), // &Vvd -> &Vvda + new Transition (9480, 9481), // &vzigz -> &vzigza + new Transition (9498, 9499), // &wedb -> &wedba + new Transition (9535, 9536), // &wre -> &wrea + new Transition (9549, 9550), // &xc -> &xca + new Transition (9572, 9577), // &xh -> &xha + new Transition (9585, 9590), // &xl -> &xla + new Transition (9594, 9595), // &xm -> &xma + new Transition (9623, 9628), // &xr -> &xra + new Transition (9665, 9666), // &Y -> &Ya + new Transition (9672, 9673), // &y -> &ya + new Transition (9747, 9748), // &Z -> &Za + new Transition (9754, 9755), // &z -> &za + new Transition (9761, 9762), // &Zc -> &Zca + new Transition (9767, 9768), // &zc -> &zca + new Transition (9800, 9801), // &ZeroWidthSp -> &ZeroWidthSpa + new Transition (9805, 9806), // &Zet -> &Zeta + new Transition (9808, 9809), // &zet -> &zeta + new Transition (9827, 9828) // &zigr -> &zigra + }; + TransitionTable_b = new Transition[96] { + new Transition (0, 301), // & -> &b + new Transition (1, 15), // &A -> &Ab + new Transition (8, 21), // &a -> &ab + new Transition (147, 150), // &angmsda -> &angmsdab + new Transition (167, 168), // &angrtv -> &angrtvb + new Transition (301, 360), // &b -> &bb + new Transition (364, 365), // &bbrkt -> &bbrktb + new Transition (613, 614), // &box -> &boxb + new Transition (735, 736), // &brv -> &brvb + new Transition (758, 760), // &bsol -> &bsolb + new Transition (764, 765), // &bsolhsu -> &bsolhsub + new Transition (805, 811), // &cap -> &capb + new Transition (1101, 1102), // &CloseCurlyDou -> &CloseCurlyDoub + new Transition (1118, 1119), // &clu -> &club + new Transition (1278, 1279), // &csu -> &csub + new Transition (1318, 1320), // &cup -> &cupb + new Transition (1432, 1463), // &d -> &db + new Transition (1577, 1578), // &DiacriticalDou -> &DiacriticalDoub + new Transition (1731, 1732), // &dou -> &doub + new Transition (1734, 1735), // &double -> &doubleb + new Transition (1744, 1745), // &Dou -> &Doub + new Transition (2023, 2024), // &dr -> &drb + new Transition (2389, 2390), // &Equili -> &Equilib + new Transition (2701, 2730), // &g -> &gb + new Transition (2708, 2724), // &G -> &Gb + new Transition (3020, 3060), // &h -> &hb + new Transition (3101, 3102), // &Hil -> &Hilb + new Transition (3166, 3167), // &hor -> &horb + new Transition (3225, 3226), // &hy -> &hyb + new Transition (3447, 3448), // &Invisi -> &Invisib + new Transition (3692, 3812), // &l -> &lb + new Transition (3723, 3724), // &Lam -> &Lamb + new Transition (3728, 3729), // &lam -> &lamb + new Transition (3766, 3768), // &larr -> &larrb + new Transition (3812, 3817), // &lb -> &lbb + new Transition (3862, 3863), // &lcu -> &lcub + new Transition (3963, 3964), // &LeftDou -> &LeftDoub + new Transition (4325, 4334), // &lh -> &lhb + new Transition (4422, 4430), // &lo -> &lob + new Transition (4579, 4584), // &low -> &lowb + new Transition (4676, 4677), // &lsq -> &lsqb + new Transition (4892, 4894), // &minus -> &minusb + new Transition (4965, 5010), // &n -> &nb + new Transition (4966, 4967), // &na -> &nab + new Transition (5398, 5399), // &NotDou -> &NotDoub + new Transition (5521, 5524), // ¬inv -> ¬invb + new Transition (5623, 5626), // ¬niv -> ¬nivb + new Transition (5701, 5702), // &NotSquareSu -> &NotSquareSub + new Transition (5726, 5727), // &NotSu -> &NotSub + new Transition (5944, 5945), // &nsqsu -> &nsqsub + new Transition (5951, 5952), // &nsu -> &nsub + new Transition (6163, 6174), // &od -> &odb + new Transition (6168, 6169), // &Od -> &Odb + new Transition (6227, 6228), // &oh -> &ohb + new Transition (6316, 6317), // &OpenCurlyDou -> &OpenCurlyDoub + new Transition (6430, 6431), // &ov -> &ovb + new Transition (6567, 6574), // &plus -> &plusb + new Transition (6876, 6996), // &r -> &rb + new Transition (6932, 6937), // &rarr -> &rarrb + new Transition (6996, 7001), // &rb -> &rbb + new Transition (7046, 7047), // &rcu -> &rcub + new Transition (7114, 7115), // &ReverseEquili -> &ReverseEquilib + new Transition (7128, 7129), // &ReverseUpEquili -> &ReverseUpEquilib + new Transition (7238, 7239), // &RightDou -> &RightDoub + new Transition (7469, 7477), // &ro -> &rob + new Transition (7559, 7560), // &rsq -> &rsqb + new Transition (7617, 7624), // &s -> &sb + new Transition (7697, 7699), // &sdot -> &sdotb + new Transition (7942, 7944), // &sol -> &solb + new Transition (7985, 7986), // &sqsu -> &sqsub + new Transition (8033, 8034), // &SquareSu -> &SquareSub + new Transition (8127, 8128), // &Su -> &Sub + new Transition (8130, 8131), // &su -> &sub + new Transition (8193, 8194), // &subsu -> &subsub + new Transition (8297, 8298), // &supdsu -> &supdsub + new Transition (8325, 8326), // &suphsu -> &suphsub + new Transition (8370, 8371), // &supsu -> &supsub + new Transition (8401, 8402), // &Ta -> &Tab + new Transition (8404, 8415), // &t -> &tb + new Transition (8578, 8580), // × -> ×b + new Transition (8594, 8596), // &top -> &topb + new Transition (8690, 8691), // &tris -> &trisb + new Transition (8768, 8797), // &U -> &Ub + new Transition (8775, 8802), // &u -> &ub + new Transition (8829, 8840), // &ud -> &udb + new Transition (8834, 8835), // &Ud -> &Udb + new Transition (8876, 8883), // &uh -> &uhb + new Transition (9039, 9040), // &UpEquili -> &UpEquilib + new Transition (9258, 9259), // &varsu -> &varsub + new Transition (9303, 9304), // &V -> &Vb + new Transition (9346, 9348), // &vee -> &veeb + new Transition (9360, 9361), // &Ver -> &Verb + new Transition (9365, 9366), // &ver -> &verb + new Transition (9427, 9428), // &vnsu -> &vnsub + new Transition (9458, 9459), // &vsu -> &vsub + new Transition (9497, 9498) // &wed -> &wedb + }; + TransitionTable_c = new Transition[377] { + new Transition (0, 796), // & -> &c + new Transition (1, 33), // &A -> &Ac + new Transition (2, 3), // &Aa -> &Aac + new Transition (8, 27), // &a -> &ac + new Transition (9, 10), // &aa -> &aac + new Transition (35, 36), // &Acir ->  + new Transition (39, 40), // &acir -> â + new Transition (99, 100), // &Ama -> &Amac + new Transition (104, 105), // &ama -> &amac + new Transition (147, 152), // &angmsda -> &angmsdac + new Transition (201, 202), // &apa -> &apac + new Transition (222, 223), // &ApplyFun -> &ApplyFunc + new Transition (247, 248), // &As -> &Asc + new Transition (251, 252), // &as -> &asc + new Transition (289, 290), // &aw -> &awc + new Transition (301, 369), // &b -> &bc + new Transition (302, 303), // &ba -> &bac + new Transition (304, 305), // &back -> &backc + new Transition (331, 374), // &B -> &Bc + new Transition (332, 333), // &Ba -> &Bac + new Transition (384, 385), // &be -> &bec + new Transition (390, 391), // &Be -> &Bec + new Transition (443, 444), // &big -> &bigc + new Transition (449, 450), // &bigcir -> &bigcirc + new Transition (472, 473), // &bigsq -> &bigsqc + new Transition (520, 521), // &bla -> &blac + new Transition (575, 576), // &blo -> &bloc + new Transition (740, 741), // &Bs -> &Bsc + new Transition (744, 745), // &bs -> &bsc + new Transition (789, 866), // &C -> &Cc + new Transition (790, 791), // &Ca -> &Cac + new Transition (796, 861), // &c -> &cc + new Transition (797, 798), // &ca -> &cac + new Transition (805, 817), // &cap -> &capc + new Transition (812, 813), // &capbr -> &capbrc + new Transition (887, 888), // &Ccir -> &Ccirc + new Transition (891, 892), // &ccir -> &ccirc + new Transition (956, 957), // &CH -> &CHc + new Transition (960, 961), // &ch -> &chc + new Transition (964, 965), // &che -> &chec + new Transition (979, 981), // &cir -> &circ + new Transition (1004, 1009), // &circled -> &circledc + new Transition (1011, 1012), // &circledcir -> &circledcirc + new Transition (1020, 1021), // &Cir -> &Circ + new Transition (1063, 1064), // &cirs -> &cirsc + new Transition (1069, 1070), // &Clo -> &Cloc + new Transition (1213, 1214), // &Coprodu -> &Coproduc + new Transition (1233, 1234), // &CounterClo -> &CounterCloc + new Transition (1270, 1271), // &Cs -> &Csc + new Transition (1274, 1275), // &cs -> &csc + new Transition (1305, 1306), // &cues -> &cuesc + new Transition (1318, 1330), // &cup -> &cupc + new Transition (1321, 1322), // &cupbr -> &cupbrc + new Transition (1359, 1360), // &curlyeqpre -> &curlyeqprec + new Transition (1363, 1364), // &curlyeqsu -> &curlyeqsuc + new Transition (1364, 1365), // &curlyeqsuc -> &curlyeqsucc + new Transition (1407, 1408), // &cw -> &cwc + new Transition (1420, 1421), // &cyl -> &cylc + new Transition (1425, 1474), // &D -> &Dc + new Transition (1432, 1480), // &d -> &dc + new Transition (1471, 1472), // &dbla -> &dblac + new Transition (1558, 1559), // &Dia -> &Diac + new Transition (1563, 1564), // &Diacriti -> &Diacritic + new Transition (1567, 1568), // &DiacriticalA -> &DiacriticalAc + new Transition (1581, 1582), // &DiacriticalDoubleA -> &DiacriticalDoubleAc + new Transition (1661, 1662), // &DJ -> &DJc + new Transition (1665, 1666), // &dj -> &djc + new Transition (1669, 1670), // &dl -> &dlc + new Transition (1873, 1874), // &DoubleVerti -> &DoubleVertic + new Transition (1960, 1961), // &DownLeftRightVe -> &DownLeftRightVec + new Transition (1970, 1971), // &DownLeftTeeVe -> &DownLeftTeeVec + new Transition (1977, 1978), // &DownLeftVe -> &DownLeftVec + new Transition (1996, 1997), // &DownRightTeeVe -> &DownRightTeeVec + new Transition (2003, 2004), // &DownRightVe -> &DownRightVec + new Transition (2023, 2031), // &dr -> &drc + new Transition (2040, 2041), // &Ds -> &Dsc + new Transition (2044, 2045), // &ds -> &dsc + new Transition (2048, 2049), // &DS -> &DSc + new Transition (2093, 2094), // &DZ -> &DZc + new Transition (2097, 2098), // &dz -> &dzc + new Transition (2108, 2127), // &E -> &Ec + new Transition (2109, 2110), // &Ea -> &Eac + new Transition (2115, 2133), // &e -> &ec + new Transition (2116, 2117), // &ea -> &eac + new Transition (2140, 2146), // &ecir -> ê + new Transition (2143, 2144), // &Ecir -> Ê + new Transition (2229, 2230), // &Ema -> &Emac + new Transition (2234, 2235), // &ema -> &emac + new Transition (2339, 2340), // &eq -> &eqc + new Transition (2342, 2343), // &eqcir -> &eqcirc + new Transition (2418, 2419), // &Es -> &Esc + new Transition (2422, 2423), // &es -> &esc + new Transition (2458, 2459), // &ex -> &exc + new Transition (2473, 2474), // &expe -> &expec + new Transition (2503, 2521), // &f -> &fc + new Transition (2517, 2518), // &F -> &Fc + new Transition (2648, 2649), // &fra -> &frac + new Transition (2693, 2694), // &Fs -> &Fsc + new Transition (2697, 2698), // &fs -> &fsc + new Transition (2701, 2746), // &g -> &gc + new Transition (2702, 2703), // &ga -> &gac + new Transition (2708, 2736), // &G -> &Gc + new Transition (2743, 2744), // &Gcir -> &Gcirc + new Transition (2748, 2749), // &gcir -> &gcirc + new Transition (2781, 2783), // &ges -> &gesc + new Transition (2783, 2784), // &gesc -> &gescc + new Transition (2816, 2817), // &GJ -> &GJc + new Transition (2820, 2821), // &gj -> &gjc + new Transition (2923, 2924), // &Gs -> &Gsc + new Transition (2927, 2928), // &gs -> &gsc + new Transition (2942, 2944), // > -> >c + new Transition (2944, 2945), // >c -> >cc + new Transition (3014, 3064), // &H -> &Hc + new Transition (3015, 3016), // &Ha -> &Hac + new Transition (3020, 3069), // &h -> &hc + new Transition (3037, 3038), // &HARD -> &HARDc + new Transition (3042, 3043), // &hard -> &hardc + new Transition (3050, 3052), // &harr -> &harrc + new Transition (3066, 3067), // &Hcir -> &Hcirc + new Transition (3071, 3072), // &hcir -> &hcirc + new Transition (3089, 3090), // &her -> &herc + new Transition (3108, 3109), // &HilbertSpa -> &HilbertSpac + new Transition (3184, 3185), // &Hs -> &Hsc + new Transition (3188, 3189), // &hs -> &hsc + new Transition (3236, 3252), // &I -> &Ic + new Transition (3237, 3238), // &Ia -> &Iac + new Transition (3243, 3250), // &i -> &ic + new Transition (3244, 3245), // &ia -> &iac + new Transition (3254, 3255), // &Icir -> Î + new Transition (3258, 3259), // &icir -> î + new Transition (3269, 3270), // &IE -> &IEc + new Transition (3273, 3274), // &ie -> &iec + new Transition (3277, 3278), // &iex -> &iexc + new Transition (3332, 3333), // &Ima -> &Imac + new Transition (3337, 3338), // &ima -> &imac + new Transition (3378, 3380), // &in -> &inc + new Transition (3401, 3403), // &int -> &intc + new Transition (3419, 3420), // &inter -> &interc + new Transition (3426, 3427), // &Interse -> &Intersec + new Transition (3463, 3464), // &IO -> &IOc + new Transition (3467, 3468), // &io -> &ioc + new Transition (3503, 3504), // &Is -> &Isc + new Transition (3507, 3508), // &is -> &isc + new Transition (3540, 3541), // &Iuk -> &Iukc + new Transition (3545, 3546), // &iuk -> &iukc + new Transition (3555, 3556), // &J -> &Jc + new Transition (3558, 3559), // &Jcir -> &Jcirc + new Transition (3561, 3562), // &j -> &jc + new Transition (3564, 3565), // &jcir -> &jcirc + new Transition (3590, 3591), // &Js -> &Jsc + new Transition (3594, 3595), // &js -> &jsc + new Transition (3599, 3600), // &Jser -> &Jserc + new Transition (3604, 3605), // &jser -> &jserc + new Transition (3609, 3610), // &Juk -> &Jukc + new Transition (3614, 3615), // &juk -> &jukc + new Transition (3618, 3632), // &K -> &Kc + new Transition (3624, 3638), // &k -> &kc + new Transition (3660, 3661), // &KH -> &KHc + new Transition (3664, 3665), // &kh -> &khc + new Transition (3668, 3669), // &KJ -> &KJc + new Transition (3672, 3673), // &kj -> &kjc + new Transition (3684, 3685), // &Ks -> &Ksc + new Transition (3688, 3689), // &ks -> &ksc + new Transition (3692, 3843), // &l -> &lc + new Transition (3698, 3837), // &L -> &Lc + new Transition (3699, 3700), // &La -> &Lac + new Transition (3705, 3706), // &la -> &lac + new Transition (3748, 3749), // &Lapla -> &Laplac + new Transition (3822, 3823), // &lbra -> &lbrac + new Transition (3869, 3870), // &ld -> &ldc + new Transition (3908, 3909), // &LeftAngleBra -> &LeftAngleBrac + new Transition (3969, 3970), // &LeftDoubleBra -> &LeftDoubleBrac + new Transition (3981, 3982), // &LeftDownTeeVe -> &LeftDownTeeVec + new Transition (3988, 3989), // &LeftDownVe -> &LeftDownVec + new Transition (4086, 4087), // &LeftRightVe -> &LeftRightVec + new Transition (4103, 4104), // &LeftTeeVe -> &LeftTeeVec + new Transition (4145, 4146), // &LeftUpDownVe -> &LeftUpDownVec + new Transition (4155, 4156), // &LeftUpTeeVe -> &LeftUpTeeVec + new Transition (4162, 4163), // &LeftUpVe -> &LeftUpVec + new Transition (4173, 4174), // &LeftVe -> &LeftVec + new Transition (4197, 4199), // &les -> &lesc + new Transition (4199, 4200), // &lesc -> &lescc + new Transition (4338, 4339), // &LJ -> &LJc + new Transition (4342, 4343), // &lj -> &ljc + new Transition (4348, 4354), // &ll -> &llc + new Transition (4396, 4397), // &lmousta -> &lmoustac + new Transition (4628, 4633), // &lr -> &lrc + new Transition (4652, 4662), // &ls -> &lsc + new Transition (4658, 4659), // &Ls -> &Lsc + new Transition (4698, 4700), // < -> <c + new Transition (4700, 4701), // <c -> <cc + new Transition (4767, 4809), // &m -> &mc + new Transition (4768, 4769), // &ma -> &mac + new Transition (4781, 4815), // &M -> &Mc + new Transition (4850, 4851), // &MediumSpa -> &MediumSpac + new Transition (4871, 4872), // &mi -> &mic + new Transition (4876, 4882), // &mid -> &midc + new Transition (4909, 4910), // &ml -> &mlc + new Transition (4937, 4938), // &Ms -> &Msc + new Transition (4941, 4942), // &ms -> &msc + new Transition (4965, 5020), // &n -> &nc + new Transition (4966, 4978), // &na -> &nac + new Transition (4971, 5024), // &N -> &Nc + new Transition (4972, 4973), // &Na -> &Nac + new Transition (5099, 5100), // &NegativeMediumSpa -> &NegativeMediumSpac + new Transition (5105, 5106), // &NegativeThi -> &NegativeThic + new Transition (5110, 5111), // &NegativeThickSpa -> &NegativeThickSpac + new Transition (5117, 5118), // &NegativeThinSpa -> &NegativeThinSpac + new Transition (5131, 5132), // &NegativeVeryThinSpa -> &NegativeVeryThinSpac + new Transition (5248, 5249), // &NJ -> &NJc + new Transition (5252, 5253), // &nj -> &njc + new Transition (5365, 5366), // &NonBreakingSpa -> &NonBreakingSpac + new Transition (5406, 5407), // &NotDoubleVerti -> &NotDoubleVertic + new Transition (5521, 5526), // ¬inv -> ¬invc + new Transition (5623, 5628), // ¬niv -> ¬nivc + new Transition (5632, 5633), // &NotPre -> &NotPrec + new Transition (5726, 5738), // &NotSu -> &NotSuc + new Transition (5738, 5739), // &NotSuc -> &NotSucc + new Transition (5813, 5814), // &NotVerti -> &NotVertic + new Transition (5842, 5844), // &npr -> &nprc + new Transition (5848, 5850), // &npre -> &nprec + new Transition (5862, 5864), // &nrarr -> &nrarrc + new Transition (5895, 5896), // &ns -> &nsc + new Transition (5896, 5898), // &nsc -> &nscc + new Transition (5904, 5905), // &Ns -> &Nsc + new Transition (5951, 5967), // &nsu -> &nsuc + new Transition (5967, 5968), // &nsuc -> &nsucc + new Transition (6131, 6152), // &O -> &Oc + new Transition (6132, 6133), // &Oa -> &Oac + new Transition (6138, 6148), // &o -> &oc + new Transition (6139, 6140), // &oa -> &oac + new Transition (6150, 6157), // &ocir -> ô + new Transition (6154, 6155), // &Ocir -> Ô + new Transition (6171, 6172), // &Odbla -> &Odblac + new Transition (6176, 6177), // &odbla -> &odblac + new Transition (6200, 6201), // &of -> &ofc + new Transition (6238, 6243), // &ol -> &olc + new Transition (6259, 6260), // &Oma -> &Omac + new Transition (6264, 6265), // &oma -> &omac + new Transition (6276, 6277), // &Omi -> &Omic + new Transition (6282, 6283), // &omi -> &omic + new Transition (6378, 6379), // &Os -> &Osc + new Transition (6382, 6383), // &os -> &osc + new Transition (6443, 6444), // &OverBra -> &OverBrac + new Transition (6463, 6494), // &p -> &pc + new Transition (6482, 6491), // &P -> &Pc + new Transition (6498, 6499), // &per -> &perc + new Transition (6545, 6546), // &pit -> &pitc + new Transition (6557, 6558), // &plan -> &planc + new Transition (6567, 6576), // &plus -> &plusc + new Transition (6569, 6570), // &plusa -> &plusac + new Transition (6611, 6612), // &Poin -> &Poinc + new Transition (6642, 6647), // &pr -> &prc + new Transition (6653, 6655), // &pre -> &prec + new Transition (6655, 6664), // &prec -> &precc + new Transition (6672, 6673), // &Pre -> &Prec + new Transition (6750, 6751), // &Produ -> &Produc + new Transition (6795, 6796), // &Ps -> &Psc + new Transition (6799, 6800), // &ps -> &psc + new Transition (6808, 6809), // &pun -> &punc + new Transition (6839, 6840), // &Qs -> &Qsc + new Transition (6843, 6844), // &qs -> &qsc + new Transition (6876, 7027), // &r -> &rc + new Transition (6882, 6883), // &ra -> &rac + new Transition (6886, 7021), // &R -> &Rc + new Transition (6887, 6888), // &Ra -> &Rac + new Transition (6898, 6899), // &radi -> &radic + new Transition (6932, 6942), // &rarr -> &rarrc + new Transition (7006, 7007), // &rbra -> &rbrac + new Transition (7053, 7054), // &rd -> &rdc + new Transition (7074, 7089), // &re -> &rec + new Transition (7182, 7183), // &RightAngleBra -> &RightAngleBrac + new Transition (7244, 7245), // &RightDoubleBra -> &RightDoubleBrac + new Transition (7256, 7257), // &RightDownTeeVe -> &RightDownTeeVec + new Transition (7263, 7264), // &RightDownVe -> &RightDownVec + new Transition (7348, 7349), // &RightTeeVe -> &RightTeeVec + new Transition (7390, 7391), // &RightUpDownVe -> &RightUpDownVec + new Transition (7400, 7401), // &RightUpTeeVe -> &RightUpTeeVec + new Transition (7407, 7408), // &RightUpVe -> &RightUpVec + new Transition (7418, 7419), // &RightVe -> &RightVec + new Transition (7459, 7460), // &rmousta -> &rmoustac + new Transition (7542, 7552), // &rs -> &rsc + new Transition (7548, 7549), // &Rs -> &Rsc + new Transition (7610, 7629), // &S -> &Sc + new Transition (7611, 7612), // &Sa -> &Sac + new Transition (7617, 7631), // &s -> &sc + new Transition (7618, 7619), // &sa -> &sac + new Transition (7631, 7645), // &sc -> &scc + new Transition (7663, 7664), // &Scir -> &Scirc + new Transition (7667, 7668), // &scir -> &scirc + new Transition (7703, 7718), // &se -> &sec + new Transition (7751, 7762), // &sh -> &shc + new Transition (7756, 7767), // &SH -> &SHc + new Transition (7758, 7759), // &SHCH -> &SHCHc + new Transition (7763, 7764), // &shch -> &shchc + new Transition (7889, 7890), // &SmallCir -> &SmallCirc + new Transition (7932, 7933), // &SOFT -> &SOFTc + new Transition (7938, 7939), // &soft -> &softc + new Transition (7968, 7969), // &sq -> &sqc + new Transition (8025, 8026), // &SquareInterse -> &SquareIntersec + new Transition (8073, 8074), // &Ss -> &Ssc + new Transition (8077, 8078), // &ss -> &ssc + new Transition (8127, 8216), // &Su -> &Suc + new Transition (8130, 8198), // &su -> &suc + new Transition (8198, 8199), // &suc -> &succ + new Transition (8199, 8208), // &succ -> &succc + new Transition (8216, 8217), // &Suc -> &Succ + new Transition (8400, 8419), // &T -> &Tc + new Transition (8404, 8425), // &t -> &tc + new Transition (8452, 8453), // &telre -> &telrec + new Transition (8493, 8494), // &thi -> &thic + new Transition (8507, 8508), // &Thi -> &Thic + new Transition (8512, 8513), // &ThickSpa -> &ThickSpac + new Transition (8523, 8524), // &ThinSpa -> &ThinSpac + new Transition (8594, 8600), // &top -> &topc + new Transition (8705, 8706), // &Ts -> &Tsc + new Transition (8709, 8710), // &ts -> &tsc + new Transition (8713, 8714), // &TS -> &TSc + new Transition (8719, 8720), // &TSH -> &TSHc + new Transition (8723, 8724), // &tsh -> &tshc + new Transition (8768, 8815), // &U -> &Uc + new Transition (8769, 8770), // &Ua -> &Uac + new Transition (8775, 8820), // &u -> &uc + new Transition (8776, 8777), // &ua -> &uac + new Transition (8792, 8793), // &Uarro -> &Uarroc + new Transition (8798, 8799), // &Ubr -> &Ubrc + new Transition (8803, 8804), // &ubr -> &ubrc + new Transition (8817, 8818), // &Ucir -> Û + new Transition (8822, 8823), // &ucir -> û + new Transition (8837, 8838), // &Udbla -> &Udblac + new Transition (8842, 8843), // &udbla -> &udblac + new Transition (8887, 8888), // &ul -> &ulc + new Transition (8905, 8906), // &Uma -> &Umac + new Transition (8910, 8911), // &uma -> &umac + new Transition (8925, 8926), // &UnderBra -> &UnderBrac + new Transition (9127, 9128), // &ur -> &urc + new Transition (9153, 9154), // &Us -> &Usc + new Transition (9157, 9158), // &us -> &usc + new Transition (9201, 9317), // &v -> &vc + new Transition (9303, 9314), // &V -> &Vc + new Transition (9374, 9375), // &Verti -> &Vertic + new Transition (9410, 9411), // &VeryThinSpa -> &VeryThinSpac + new Transition (9450, 9451), // &Vs -> &Vsc + new Transition (9454, 9455), // &vs -> &vsc + new Transition (9484, 9485), // &W -> &Wc + new Transition (9487, 9488), // &Wcir -> &Wcirc + new Transition (9490, 9491), // &w -> &wc + new Transition (9493, 9494), // &wcir -> &wcirc + new Transition (9540, 9541), // &Ws -> &Wsc + new Transition (9544, 9545), // &ws -> &wsc + new Transition (9548, 9549), // &x -> &xc + new Transition (9554, 9555), // &xcir -> &xcirc + new Transition (9632, 9633), // &Xs -> &Xsc + new Transition (9636, 9637), // &xs -> &xsc + new Transition (9640, 9641), // &xsq -> &xsqc + new Transition (9665, 9685), // &Y -> &Yc + new Transition (9666, 9667), // &Ya -> &Yac + new Transition (9672, 9690), // &y -> &yc + new Transition (9673, 9674), // &ya -> &yac + new Transition (9679, 9680), // &YA -> &YAc + new Transition (9687, 9688), // &Ycir -> &Ycirc + new Transition (9692, 9693), // &ycir -> &ycirc + new Transition (9708, 9709), // &YI -> &YIc + new Transition (9712, 9713), // &yi -> &yic + new Transition (9724, 9725), // &Ys -> &Ysc + new Transition (9728, 9729), // &ys -> &ysc + new Transition (9732, 9733), // &YU -> &YUc + new Transition (9736, 9737), // &yu -> &yuc + new Transition (9747, 9761), // &Z -> &Zc + new Transition (9748, 9749), // &Za -> &Zac + new Transition (9754, 9767), // &z -> &zc + new Transition (9755, 9756), // &za -> &zac + new Transition (9801, 9802), // &ZeroWidthSpa -> &ZeroWidthSpac + new Transition (9817, 9818), // &ZH -> &ZHc + new Transition (9821, 9822), // &zh -> &zhc + new Transition (9840, 9841), // &Zs -> &Zsc + new Transition (9844, 9845) // &zs -> &zsc + }; + TransitionTable_d = new Transition[206] { + new Transition (0, 1432), // & -> &d + new Transition (27, 29), // &ac -> &acd + new Transition (116, 117), // &An -> &And + new Transition (119, 120), // &an -> &and + new Transition (120, 126), // &and -> &andd + new Transition (123, 124), // &andan -> &andand + new Transition (144, 145), // &angms -> &angmsd + new Transition (147, 154), // &angmsda -> &angmsdad + new Transition (168, 170), // &angrtvb -> &angrtvbd + new Transition (210, 211), // &api -> &apid + new Transition (271, 272), // &Atil -> &Atild + new Transition (277, 278), // &atil -> &atild + new Transition (301, 379), // &b -> &bd + new Transition (350, 351), // &Barwe -> &Barwed + new Transition (354, 355), // &barwe -> &barwed + new Transition (455, 456), // &bigo -> &bigod + new Transition (488, 489), // &bigtriangle -> &bigtriangled + new Transition (508, 509), // &bigwe -> &bigwed + new Transition (545, 547), // &blacktriangle -> &blacktriangled + new Transition (613, 623), // &box -> &boxd + new Transition (636, 642), // &boxH -> &boxHd + new Transition (638, 646), // &boxh -> &boxhd + new Transition (789, 907), // &C -> &Cd + new Transition (796, 911), // &c -> &cd + new Transition (805, 824), // &cap -> &capd + new Transition (808, 809), // &capan -> &capand + new Transition (876, 877), // &Cce -> &Cced + new Transition (881, 882), // &cce -> &cced + new Transition (915, 916), // &ce -> &ced + new Transition (920, 921), // &Ce -> &Ced + new Transition (945, 946), // ¢er -> ¢erd + new Transition (987, 1004), // &circle -> &circled + new Transition (1004, 1014), // &circled -> &circledd + new Transition (1060, 1061), // &cirmi -> &cirmid + new Transition (1165, 1167), // &cong -> &congd + new Transition (1207, 1208), // &copro -> &coprod + new Transition (1211, 1212), // &Copro -> &Coprod + new Transition (1287, 1288), // &ct -> &ctd + new Transition (1292, 1293), // &cu -> &cud + new Transition (1318, 1337), // &cup -> &cupd + new Transition (1372, 1373), // &curlywe -> &curlywed + new Transition (1404, 1405), // &cuwe -> &cuwed + new Transition (1432, 1492), // &d -> &dd + new Transition (1507, 1508), // &DDotrah -> &DDotrahd + new Transition (1595, 1596), // &DiacriticalTil -> &DiacriticalTild + new Transition (1605, 1606), // &Diamon -> &Diamond + new Transition (1609, 1610), // &diamon -> &diamond + new Transition (1645, 1646), // &divi -> &divid + new Transition (1701, 1703), // &doteq -> &doteqd + new Transition (1739, 1740), // &doublebarwe -> &doublebarwed + new Transition (1896, 1921), // &down -> &downd + new Transition (2067, 2068), // &dt -> &dtd + new Transition (2108, 2162), // &E -> &Ed + new Transition (2115, 2169), // &e -> &ed + new Transition (2198, 2200), // &egs -> &egsd + new Transition (2222, 2224), // &els -> &elsd + new Transition (2379, 2380), // &EqualTil -> &EqualTild + new Transition (2422, 2426), // &es -> &esd + new Transition (2509, 2510), // &falling -> &fallingd + new Transition (2557, 2558), // &Fille -> &Filled + new Transition (2701, 2759), // &g -> &gd + new Transition (2708, 2755), // &G -> &Gd + new Transition (2712, 2718), // &Gamma -> &Gammad + new Transition (2716, 2720), // &gamma -> &gammad + new Transition (2737, 2738), // &Gce -> &Gced + new Transition (2781, 2786), // &ges -> &gesd + new Transition (2919, 2920), // &GreaterTil -> &GreaterTild + new Transition (2942, 2950), // > -> >d + new Transition (2965, 2976), // >r -> >rd + new Transition (3041, 3042), // &har -> &hard + new Transition (3236, 3265), // &I -> &Id + new Transition (3369, 3370), // &impe -> &imped + new Transition (3393, 3394), // &ino -> &inod + new Transition (3441, 3442), // &intpro -> &intprod + new Transition (3494, 3495), // &ipro -> &iprod + new Transition (3512, 3514), // &isin -> &isind + new Transition (3530, 3531), // &Itil -> &Itild + new Transition (3535, 3536), // &itil -> &itild + new Transition (3633, 3634), // &Kce -> &Kced + new Transition (3639, 3640), // &kce -> &kced + new Transition (3692, 3869), // &l -> &ld + new Transition (3724, 3725), // &Lamb -> &Lambd + new Transition (3729, 3730), // &lamb -> &lambd + new Transition (3737, 3739), // &lang -> &langd + new Transition (3832, 3833), // &lbrksl -> &lbrksld + new Transition (3849, 3850), // &Lce -> &Lced + new Transition (3854, 3855), // &lce -> &lced + new Transition (3879, 3880), // &ldr -> &ldrd + new Transition (4010, 4011), // &leftharpoon -> &leftharpoond + new Transition (4197, 4202), // &les -> &lesd + new Transition (4215, 4223), // &less -> &lessd + new Transition (4297, 4298), // &LessTil -> &LessTild + new Transition (4327, 4328), // &lhar -> &lhard + new Transition (4372, 4373), // &llhar -> &llhard + new Transition (4380, 4381), // &Lmi -> &Lmid + new Transition (4386, 4387), // &lmi -> &lmid + new Transition (4642, 4644), // &lrhar -> &lrhard + new Transition (4698, 4706), // < -> <d + new Transition (4743, 4744), // &lur -> &lurd + new Transition (4767, 4820), // &m -> &md + new Transition (4789, 4791), // &mapsto -> &mapstod + new Transition (4835, 4836), // &measure -> &measured + new Transition (4843, 4844), // &Me -> &Med + new Transition (4871, 4876), // &mi -> &mid + new Transition (4876, 4886), // &mid -> &midd + new Transition (4892, 4896), // &minus -> &minusd + new Transition (4909, 4913), // &ml -> &mld + new Transition (4922, 4923), // &mo -> &mod + new Transition (4965, 5059), // &n -> &nd + new Transition (4990, 4991), // &napi -> &napid + new Transition (5034, 5035), // &Nce -> &Nced + new Transition (5039, 5040), // &nce -> &nced + new Transition (5046, 5048), // &ncong -> &ncongd + new Transition (5064, 5080), // &ne -> &ned + new Transition (5092, 5093), // &NegativeMe -> &NegativeMed + new Transition (5150, 5151), // &Neste -> &Nested + new Transition (5242, 5244), // &nis -> &nisd + new Transition (5256, 5265), // &nl -> &nld + new Transition (5344, 5345), // &nmi -> &nmid + new Transition (5429, 5430), // &NotEqualTil -> &NotEqualTild + new Transition (5489, 5490), // &NotGreaterTil -> &NotGreaterTild + new Transition (5513, 5515), // ¬in -> ¬ind + new Transition (5586, 5587), // &NotLessTil -> &NotLessTild + new Transition (5594, 5595), // &NotNeste -> &NotNested + new Transition (5634, 5635), // &NotPrece -> &NotPreced + new Transition (5741, 5742), // &NotSuccee -> &NotSucceed + new Transition (5764, 5765), // &NotSucceedsTil -> &NotSucceedsTild + new Transition (5783, 5784), // &NotTil -> &NotTild + new Transition (5805, 5806), // &NotTildeTil -> &NotTildeTild + new Transition (5915, 5916), // &nshortmi -> &nshortmid + new Transition (5935, 5936), // &nsmi -> &nsmid + new Transition (5994, 5995), // &Ntil -> &Ntild + new Transition (5999, 6000), // &ntil -> &ntild + new Transition (6043, 6063), // &nv -> &nvd + new Transition (6047, 6053), // &nV -> &nVd + new Transition (6131, 6168), // &O -> &Od + new Transition (6138, 6163), // &o -> &od + new Transition (6187, 6188), // &odsol -> &odsold + new Transition (6282, 6288), // &omi -> &omid + new Transition (6342, 6348), // &or -> &ord + new Transition (6401, 6402), // &Otil -> &Otild + new Transition (6407, 6408), // &otil -> &otild + new Transition (6504, 6505), // &perio -> &period + new Transition (6567, 6580), // &plus -> &plusd + new Transition (6637, 6638), // &poun -> £ + new Transition (6674, 6675), // &Prece -> &Preced + new Transition (6698, 6699), // &PrecedesTil -> &PrecedesTild + new Transition (6745, 6746), // &pro -> &prod + new Transition (6748, 6749), // &Pro -> &Prod + new Transition (6876, 7053), // &r -> &rd + new Transition (6882, 6897), // &ra -> &rad + new Transition (6912, 6914), // &rang -> &rangd + new Transition (7016, 7017), // &rbrksl -> &rbrksld + new Transition (7033, 7034), // &Rce -> &Rced + new Transition (7038, 7039), // &rce -> &rced + new Transition (7057, 7058), // &rdl -> &rdld + new Transition (7157, 7158), // &rhar -> &rhard + new Transition (7285, 7286), // &rightharpoon -> &rightharpoond + new Transition (7434, 7435), // &rising -> &risingd + new Transition (7466, 7467), // &rnmi -> &rnmid + new Transition (7502, 7503), // &Roun -> &Round + new Transition (7598, 7599), // &RuleDelaye -> &RuleDelayed + new Transition (7617, 7695), // &s -> &sd + new Transition (7651, 7658), // &sce -> &sced + new Transition (7653, 7654), // &Sce -> &Sced + new Transition (7800, 7801), // &shortmi -> &shortmid + new Transition (7847, 7849), // &sim -> &simd + new Transition (7918, 7919), // &smi -> &smid + new Transition (7957, 7958), // &spa -> &spad + new Transition (8131, 8133), // &sub -> &subd + new Transition (8139, 8141), // &sube -> &subed + new Transition (8219, 8220), // &Succee -> &Succeed + new Transition (8242, 8243), // &SucceedsTil -> &SucceedsTild + new Transition (8284, 8292), // &sup -> &supd + new Transition (8302, 8304), // &supe -> &suped + new Transition (8404, 8445), // &t -> &td + new Transition (8431, 8432), // &Tce -> &Tced + new Transition (8436, 8437), // &tce -> &tced + new Transition (8545, 8546), // &Til -> &Tild + new Transition (8550, 8551), // &til -> &tild + new Transition (8572, 8573), // &TildeTil -> &TildeTild + new Transition (8578, 8585), // × -> ×d + new Transition (8629, 8630), // &tra -> &trad + new Transition (8633, 8664), // &tri -> &trid + new Transition (8638, 8640), // &triangle -> &triangled + new Transition (8745, 8746), // &twohea -> &twohead + new Transition (8768, 8834), // &U -> &Ud + new Transition (8775, 8829), // &u -> &ud + new Transition (8916, 8917), // &Un -> &Und + new Transition (8970, 9014), // &Up -> &Upd + new Transition (8983, 9024), // &up -> &upd + new Transition (9161, 9162), // &ut -> &utd + new Transition (9168, 9169), // &Util -> &Utild + new Transition (9173, 9174), // &util -> &utild + new Transition (9201, 9335), // &v -> &vd + new Transition (9303, 9325), // &V -> &Vd + new Transition (9399, 9400), // &VerticalTil -> &VerticalTild + new Transition (9471, 9472), // &Vv -> &Vvd + new Transition (9496, 9497), // &we -> &wed + new Transition (9502, 9503), // &We -> &Wed + new Transition (9548, 9560), // &x -> &xd + new Transition (9602, 9603), // &xo -> &xod + new Transition (9660, 9661), // &xwe -> &xwed + new Transition (9747, 9777), // &Z -> &Zd + new Transition (9754, 9781), // &z -> &zd + new Transition (9795, 9796) // &ZeroWi -> &ZeroWid + }; + TransitionTable_e = new Transition[674] { + new Transition (0, 2115), // & -> &e + new Transition (5, 6), // &Aacut -> Á + new Transition (8, 55), // &a -> &ae + new Transition (12, 13), // &aacut -> á + new Transition (16, 17), // &Abr -> &Abre + new Transition (18, 19), // &Abrev -> &Abreve + new Transition (22, 23), // &abr -> &abre + new Transition (24, 25), // &abrev -> &abreve + new Transition (43, 44), // &acut -> ´ + new Transition (70, 71), // &Agrav -> À + new Transition (76, 77), // &agrav -> à + new Transition (79, 80), // &al -> &ale + new Transition (131, 132), // &andslop -> &andslope + new Transition (136, 138), // &ang -> &ange + new Transition (140, 141), // &angl -> &angle + new Transition (147, 156), // &angmsda -> &angmsdae + new Transition (199, 208), // &ap -> &ape + new Transition (232, 234), // &approx -> &approxe + new Transition (264, 266), // &asymp -> &asympe + new Transition (272, 273), // &Atild -> à + new Transition (278, 279), // &atild -> ã + new Transition (301, 384), // &b -> &be + new Transition (304, 310), // &back -> &backe + new Transition (321, 322), // &backprim -> &backprime + new Transition (326, 328), // &backsim -> &backsime + new Transition (331, 390), // &B -> &Be + new Transition (345, 346), // &barv -> &barve + new Transition (346, 347), // &barve -> &barvee + new Transition (349, 350), // &Barw -> &Barwe + new Transition (353, 354), // &barw -> &barwe + new Transition (357, 358), // &barwedg -> &barwedge + new Transition (388, 397), // &becaus -> &because + new Transition (394, 395), // &Becaus -> &Because + new Transition (431, 432), // &betw -> &betwe + new Transition (432, 433), // &betwe -> &betwee + new Transition (467, 468), // &bigotim -> &bigotime + new Transition (487, 488), // &bigtriangl -> &bigtriangle + new Transition (503, 504), // &bigv -> &bigve + new Transition (504, 505), // &bigve -> &bigvee + new Transition (507, 508), // &bigw -> &bigwe + new Transition (510, 511), // &bigwedg -> &bigwedge + new Transition (525, 526), // &blackloz -> &blackloze + new Transition (528, 529), // &blacklozeng -> &blacklozenge + new Transition (535, 536), // &blacksquar -> &blacksquare + new Transition (544, 545), // &blacktriangl -> &blacktriangle + new Transition (552, 553), // &blacktrianglel -> &blacktrianglele + new Transition (579, 580), // &bn -> &bne + new Transition (610, 611), // &bowti -> &bowtie + new Transition (669, 670), // &boxtim -> &boxtime + new Transition (722, 723), // &bprim -> &bprime + new Transition (725, 726), // &Br -> &Bre + new Transition (727, 728), // &Brev -> &Breve + new Transition (730, 731), // &br -> &bre + new Transition (732, 733), // &brev -> &breve + new Transition (744, 748), // &bs -> &bse + new Transition (753, 755), // &bsim -> &bsime + new Transition (769, 771), // &bull -> &bulle + new Transition (775, 779), // &bump -> &bumpe + new Transition (783, 784), // &Bump -> &Bumpe + new Transition (789, 920), // &C -> &Ce + new Transition (793, 794), // &Cacut -> &Cacute + new Transition (796, 915), // &c -> &ce + new Transition (800, 801), // &cacut -> &cacute + new Transition (835, 836), // &CapitalDiff -> &CapitalDiffe + new Transition (837, 838), // &CapitalDiffer -> &CapitalDiffere + new Transition (848, 849), // &car -> &care + new Transition (856, 857), // &Cayl -> &Cayle + new Transition (861, 881), // &cc -> &cce + new Transition (866, 876), // &Cc -> &Cce + new Transition (934, 944), // ¢ -> ¢e + new Transition (937, 938), // &Cent -> &Cente + new Transition (960, 964), // &ch -> &che + new Transition (979, 1051), // &cir -> &cire + new Transition (981, 983), // &circ -> &circe + new Transition (986, 987), // &circl -> &circle + new Transition (993, 994), // &circlearrowl -> &circlearrowle + new Transition (1022, 1023), // &Circl -> &Circle + new Transition (1045, 1046), // &CircleTim -> &CircleTime + new Transition (1074, 1075), // &Clockwis -> &Clockwise + new Transition (1085, 1086), // &ClockwiseContourInt -> &ClockwiseContourInte + new Transition (1092, 1093), // &Clos -> &Close + new Transition (1103, 1104), // &CloseCurlyDoubl -> &CloseCurlyDouble + new Transition (1108, 1109), // &CloseCurlyDoubleQuot -> &CloseCurlyDoubleQuote + new Transition (1114, 1115), // &CloseCurlyQuot -> &CloseCurlyQuote + new Transition (1129, 1136), // &Colon -> &Colone + new Transition (1134, 1138), // &colon -> &colone + new Transition (1153, 1154), // &compl -> &comple + new Transition (1155, 1156), // &complem -> &compleme + new Transition (1160, 1161), // &complex -> &complexe + new Transition (1174, 1175), // &Congru -> &Congrue + new Transition (1193, 1194), // &ContourInt -> &ContourInte + new Transition (1228, 1229), // &Count -> &Counte + new Transition (1238, 1239), // &CounterClockwis -> &CounterClockwise + new Transition (1249, 1250), // &CounterClockwiseContourInt -> &CounterClockwiseContourInte + new Transition (1279, 1281), // &csub -> &csube + new Transition (1283, 1285), // &csup -> &csupe + new Transition (1292, 1301), // &cu -> &cue + new Transition (1354, 1355), // &curly -> &curlye + new Transition (1358, 1359), // &curlyeqpr -> &curlyeqpre + new Transition (1367, 1368), // &curlyv -> &curlyve + new Transition (1368, 1369), // &curlyve -> &curlyvee + new Transition (1371, 1372), // &curlyw -> &curlywe + new Transition (1374, 1375), // &curlywedg -> &curlywedge + new Transition (1377, 1378), // &curr -> &curre + new Transition (1381, 1382), // &curv -> &curve + new Transition (1388, 1389), // &curvearrowl -> &curvearrowle + new Transition (1399, 1400), // &cuv -> &cuve + new Transition (1400, 1401), // &cuve -> &cuvee + new Transition (1403, 1404), // &cuw -> &cuwe + new Transition (1425, 1519), // &D -> &De + new Transition (1428, 1429), // &Dagg -> &Dagge + new Transition (1432, 1516), // &d -> &de + new Transition (1435, 1436), // &dagg -> &dagge + new Transition (1439, 1440), // &dal -> &dale + new Transition (1496, 1497), // &ddagg -> &ddagge + new Transition (1512, 1513), // &ddots -> &ddotse + new Transition (1570, 1571), // &DiacriticalAcut -> &DiacriticalAcute + new Transition (1579, 1580), // &DiacriticalDoubl -> &DiacriticalDouble + new Transition (1584, 1585), // &DiacriticalDoubleAcut -> &DiacriticalDoubleAcute + new Transition (1590, 1591), // &DiacriticalGrav -> &DiacriticalGrave + new Transition (1596, 1597), // &DiacriticalTild -> &DiacriticalTilde + new Transition (1599, 1619), // &di -> &die + new Transition (1622, 1623), // &Diff -> &Diffe + new Transition (1624, 1625), // &Differ -> &Differe + new Transition (1646, 1647), // &divid -> ÷ + new Transition (1653, 1654), // ÷ontim -> ÷ontime + new Transition (1694, 1700), // &dot -> &dote + new Transition (1728, 1729), // &dotsquar -> &dotsquare + new Transition (1733, 1734), // &doubl -> &double + new Transition (1738, 1739), // &doublebarw -> &doublebarwe + new Transition (1741, 1742), // &doublebarwedg -> &doublebarwedge + new Transition (1746, 1747), // &Doubl -> &Double + new Transition (1757, 1758), // &DoubleContourInt -> &DoubleContourInte + new Transition (1776, 1777), // &DoubleL -> &DoubleLe + new Transition (1797, 1798), // &DoubleLeftT -> &DoubleLeftTe + new Transition (1798, 1799), // &DoubleLeftTe -> &DoubleLeftTee + new Transition (1804, 1805), // &DoubleLongL -> &DoubleLongLe + new Transition (1847, 1848), // &DoubleRightT -> &DoubleRightTe + new Transition (1848, 1849), // &DoubleRightTe -> &DoubleRightTee + new Transition (1869, 1870), // &DoubleV -> &DoubleVe + new Transition (1916, 1917), // &DownBr -> &DownBre + new Transition (1918, 1919), // &DownBrev -> &DownBreve + new Transition (1939, 1940), // &downharpoonl -> &downharpoonle + new Transition (1950, 1951), // &DownL -> &DownLe + new Transition (1959, 1960), // &DownLeftRightV -> &DownLeftRightVe + new Transition (1966, 1967), // &DownLeftT -> &DownLeftTe + new Transition (1967, 1968), // &DownLeftTe -> &DownLeftTee + new Transition (1969, 1970), // &DownLeftTeeV -> &DownLeftTeeVe + new Transition (1976, 1977), // &DownLeftV -> &DownLeftVe + new Transition (1992, 1993), // &DownRightT -> &DownRightTe + new Transition (1993, 1994), // &DownRightTe -> &DownRightTee + new Transition (1995, 1996), // &DownRightTeeV -> &DownRightTeeVe + new Transition (2002, 2003), // &DownRightV -> &DownRightVe + new Transition (2013, 2014), // &DownT -> &DownTe + new Transition (2014, 2015), // &DownTe -> &DownTee + new Transition (2090, 2091), // &dwangl -> &dwangle + new Transition (2112, 2113), // &Eacut -> É + new Transition (2115, 2173), // &e -> &ee + new Transition (2119, 2120), // &eacut -> é + new Transition (2123, 2124), // &east -> &easte + new Transition (2190, 2191), // &Egrav -> È + new Transition (2195, 2196), // &egrav -> è + new Transition (2206, 2207), // &El -> &Ele + new Transition (2208, 2209), // &Elem -> &Eleme + new Transition (2215, 2216), // &elint -> &elinte + new Transition (2242, 2243), // &emptys -> &emptyse + new Transition (2258, 2259), // &EmptySmallSquar -> &EmptySmallSquare + new Transition (2263, 2264), // &EmptyV -> &EmptyVe + new Transition (2276, 2277), // &EmptyVerySmallSquar -> &EmptyVerySmallSquare + new Transition (2362, 2363), // &eqslantl -> &eqslantle + new Transition (2372, 2383), // &equ -> &eque + new Transition (2380, 2381), // &EqualTild -> &EqualTilde + new Transition (2472, 2473), // &exp -> &expe + new Transition (2484, 2485), // &Expon -> &Expone + new Transition (2494, 2495), // &expon -> &expone + new Transition (2500, 2501), // &exponential -> &exponentiale + new Transition (2503, 2524), // &f -> &fe + new Transition (2513, 2514), // &fallingdots -> &fallingdotse + new Transition (2527, 2528), // &femal -> &female + new Transition (2556, 2557), // &Fill -> &Fille + new Transition (2568, 2569), // &FilledSmallSquar -> &FilledSmallSquare + new Transition (2571, 2572), // &FilledV -> &FilledVe + new Transition (2584, 2585), // &FilledVerySmallSquar -> &FilledVerySmallSquare + new Transition (2632, 2633), // &Fouri -> &Fourie + new Transition (2701, 2765), // &g -> &ge + new Transition (2705, 2706), // &gacut -> &gacute + new Transition (2725, 2726), // &Gbr -> &Gbre + new Transition (2727, 2728), // &Gbrev -> &Gbreve + new Transition (2731, 2732), // &gbr -> &gbre + new Transition (2733, 2734), // &gbrev -> &gbreve + new Transition (2736, 2737), // &Gc -> &Gce + new Transition (2794, 2796), // &gesl -> &gesle + new Transition (2812, 2813), // &gim -> &gime + new Transition (2832, 2843), // &gn -> &gne + new Transition (2863, 2864), // &grav -> &grave + new Transition (2866, 2867), // &Gr -> &Gre + new Transition (2869, 2870), // &Great -> &Greate + new Transition (2878, 2879), // &GreaterEqualL -> &GreaterEqualLe + new Transition (2894, 2895), // &GreaterGr -> &GreaterGre + new Transition (2897, 2898), // &GreaterGreat -> &GreaterGreate + new Transition (2901, 2902), // &GreaterL -> &GreaterLe + new Transition (2920, 2921), // &GreaterTild -> &GreaterTilde + new Transition (2932, 2934), // &gsim -> &gsime + new Transition (2960, 2961), // >qu -> >que + new Transition (2965, 2980), // >r -> >re + new Transition (2982, 2983), // >reql -> >reqle + new Transition (2988, 2989), // >reqql -> >reqqle + new Transition (2993, 2994), // >rl -> >rle + new Transition (3002, 3003), // &gv -> &gve + new Transition (3006, 3007), // &gvertn -> &gvertne + new Transition (3016, 3017), // &Hac -> &Hace + new Transition (3020, 3074), // &h -> &he + new Transition (3102, 3103), // &Hilb -> &Hilbe + new Transition (3109, 3110), // &HilbertSpac -> &HilbertSpace + new Transition (3113, 3114), // &hks -> &hkse + new Transition (3138, 3139), // &hookl -> &hookle + new Transition (3181, 3182), // &HorizontalLin -> &HorizontalLine + new Transition (3232, 3233), // &hyph -> &hyphe + new Transition (3240, 3241), // &Iacut -> Í + new Transition (3243, 3273), // &i -> &ie + new Transition (3247, 3248), // &iacut -> í + new Transition (3292, 3293), // &Igrav -> Ì + new Transition (3298, 3299), // &igrav -> ì + new Transition (3341, 3342), // &imag -> &image + new Transition (3354, 3355), // &imaglin -> &imagline + new Transition (3368, 3369), // &imp -> &impe + new Transition (3374, 3375), // &Impli -> &Implie + new Transition (3382, 3383), // &incar -> &incare + new Transition (3390, 3391), // &infinti -> &infintie + new Transition (3399, 3413), // &Int -> &Inte + new Transition (3401, 3407), // &int -> &inte + new Transition (3408, 3409), // &integ -> &intege + new Transition (3425, 3426), // &Inters -> &Interse + new Transition (3449, 3450), // &Invisibl -> &Invisible + new Transition (3459, 3460), // &InvisibleTim -> &InvisibleTime + new Transition (3498, 3499), // &iqu -> &ique + new Transition (3531, 3532), // &Itild -> &Itilde + new Transition (3536, 3537), // &itild -> &itilde + new Transition (3590, 3598), // &Js -> &Jse + new Transition (3594, 3603), // &js -> &jse + new Transition (3632, 3633), // &Kc -> &Kce + new Transition (3638, 3639), // &kc -> &kce + new Transition (3655, 3656), // &kgr -> &kgre + new Transition (3656, 3657), // &kgre -> &kgree + new Transition (3692, 3896), // &l -> &le + new Transition (3698, 3898), // &L -> &Le + new Transition (3702, 3703), // &Lacut -> &Lacute + new Transition (3705, 3711), // &la -> &lae + new Transition (3708, 3709), // &lacut -> &lacute + new Transition (3741, 3742), // &langl -> &langle + new Transition (3749, 3750), // &Laplac -> &Laplace + new Transition (3792, 3803), // &lat -> &late + new Transition (3823, 3824), // &lbrac -> &lbrace + new Transition (3828, 3829), // &lbrk -> &lbrke + new Transition (3837, 3849), // &Lc -> &Lce + new Transition (3843, 3854), // &lc -> &lce + new Transition (3904, 3905), // &LeftAngl -> &LeftAngle + new Transition (3910, 3911), // &LeftAngleBrack -> &LeftAngleBracke + new Transition (3953, 3954), // &LeftC -> &LeftCe + new Transition (3965, 3966), // &LeftDoubl -> &LeftDouble + new Transition (3971, 3972), // &LeftDoubleBrack -> &LeftDoubleBracke + new Transition (3977, 3978), // &LeftDownT -> &LeftDownTe + new Transition (3978, 3979), // &LeftDownTe -> &LeftDownTee + new Transition (3980, 3981), // &LeftDownTeeV -> &LeftDownTeeVe + new Transition (3987, 3988), // &LeftDownV -> &LeftDownVe + new Transition (4019, 4020), // &leftl -> &leftle + new Transition (4085, 4086), // &LeftRightV -> &LeftRightVe + new Transition (4092, 4093), // &LeftT -> &LeftTe + new Transition (4093, 4094), // &LeftTe -> &LeftTee + new Transition (4102, 4103), // &LeftTeeV -> &LeftTeeVe + new Transition (4111, 4112), // &leftthr -> &leftthre + new Transition (4112, 4113), // &leftthre -> &leftthree + new Transition (4116, 4117), // &leftthreetim -> &leftthreetime + new Transition (4125, 4126), // &LeftTriangl -> &LeftTriangle + new Transition (4144, 4145), // &LeftUpDownV -> &LeftUpDownVe + new Transition (4151, 4152), // &LeftUpT -> &LeftUpTe + new Transition (4152, 4153), // &LeftUpTe -> &LeftUpTee + new Transition (4154, 4155), // &LeftUpTeeV -> &LeftUpTeeVe + new Transition (4161, 4162), // &LeftUpV -> &LeftUpVe + new Transition (4172, 4173), // &LeftV -> &LeftVe + new Transition (4210, 4212), // &lesg -> &lesge + new Transition (4215, 4227), // &less -> &lesse + new Transition (4246, 4247), // &LessEqualGr -> &LessEqualGre + new Transition (4249, 4250), // &LessEqualGreat -> &LessEqualGreate + new Transition (4264, 4265), // &LessGr -> &LessGre + new Transition (4267, 4268), // &LessGreat -> &LessGreate + new Transition (4275, 4276), // &LessL -> &LessLe + new Transition (4298, 4299), // &LessTild -> &LessTilde + new Transition (4346, 4361), // &Ll -> &Lle + new Transition (4357, 4358), // &llcorn -> &llcorne + new Transition (4398, 4399), // &lmoustach -> &lmoustache + new Transition (4401, 4412), // &ln -> &lne + new Transition (4437, 4438), // &LongL -> &LongLe + new Transition (4447, 4448), // &Longl -> &Longle + new Transition (4459, 4460), // &longl -> &longle + new Transition (4549, 4550), // &looparrowl -> &looparrowle + new Transition (4575, 4576), // &lotim -> &lotime + new Transition (4588, 4589), // &Low -> &Lowe + new Transition (4591, 4592), // &LowerL -> &LowerLe + new Transition (4612, 4614), // &loz -> &loze + new Transition (4616, 4617), // &lozeng -> &lozenge + new Transition (4636, 4637), // &lrcorn -> &lrcorne + new Transition (4670, 4672), // &lsim -> &lsime + new Transition (4711, 4712), // <hr -> <hre + new Transition (4712, 4713), // <hre -> <hree + new Transition (4716, 4717), // <im -> <ime + new Transition (4726, 4727), // <qu -> <que + new Transition (4732, 4734), // <ri -> <rie + new Transition (4755, 4756), // &lv -> &lve + new Transition (4759, 4760), // &lvertn -> &lvertne + new Transition (4767, 4830), // &m -> &me + new Transition (4772, 4773), // &mal -> &male + new Transition (4775, 4777), // &malt -> &malte + new Transition (4778, 4779), // &maltes -> &maltese + new Transition (4781, 4843), // &M -> &Me + new Transition (4796, 4797), // &mapstol -> &mapstole + new Transition (4805, 4806), // &mark -> &marke + new Transition (4834, 4835), // &measur -> &measure + new Transition (4840, 4841), // &measuredangl -> &measuredangle + new Transition (4851, 4852), // &MediumSpac -> &MediumSpace + new Transition (4923, 4924), // &mod -> &mode + new Transition (4965, 5064), // &n -> &ne + new Transition (4971, 5084), // &N -> &Ne + new Transition (4975, 4976), // &Nacut -> &Nacute + new Transition (4980, 4981), // &nacut -> &nacute + new Transition (5016, 5018), // &nbump -> &nbumpe + new Transition (5020, 5039), // &nc -> &nce + new Transition (5024, 5034), // &Nc -> &Nce + new Transition (5089, 5090), // &Negativ -> &Negative + new Transition (5091, 5092), // &NegativeM -> &NegativeMe + new Transition (5100, 5101), // &NegativeMediumSpac -> &NegativeMediumSpace + new Transition (5111, 5112), // &NegativeThickSpac -> &NegativeThickSpace + new Transition (5118, 5119), // &NegativeThinSpac -> &NegativeThinSpace + new Transition (5121, 5122), // &NegativeV -> &NegativeVe + new Transition (5132, 5133), // &NegativeVeryThinSpac -> &NegativeVeryThinSpace + new Transition (5140, 5141), // &nes -> &nese + new Transition (5149, 5150), // &Nest -> &Neste + new Transition (5153, 5154), // &NestedGr -> &NestedGre + new Transition (5156, 5157), // &NestedGreat -> &NestedGreate + new Transition (5160, 5161), // &NestedGreaterGr -> &NestedGreaterGre + new Transition (5163, 5164), // &NestedGreaterGreat -> &NestedGreaterGreate + new Transition (5167, 5168), // &NestedL -> &NestedLe + new Transition (5171, 5172), // &NestedLessL -> &NestedLessLe + new Transition (5179, 5180), // &NewLin -> &NewLine + new Transition (5195, 5198), // &ng -> &nge + new Transition (5256, 5270), // &nl -> &nle + new Transition (5272, 5273), // &nL -> &nLe + new Transition (5337, 5339), // &nltri -> &nltrie + new Transition (5349, 5350), // &NoBr -> &NoBre + new Transition (5356, 5357), // &NonBr -> &NonBre + new Transition (5366, 5367), // &NonBreakingSpac -> &NonBreakingSpace + new Transition (5385, 5386), // &NotCongru -> &NotCongrue + new Transition (5400, 5401), // &NotDoubl -> &NotDouble + new Transition (5402, 5403), // &NotDoubleV -> &NotDoubleVe + new Transition (5415, 5416), // &NotEl -> &NotEle + new Transition (5417, 5418), // &NotElem -> &NotEleme + new Transition (5430, 5431), // &NotEqualTild -> &NotEqualTilde + new Transition (5440, 5441), // &NotGr -> &NotGre + new Transition (5443, 5444), // &NotGreat -> &NotGreate + new Transition (5464, 5465), // &NotGreaterGr -> &NotGreaterGre + new Transition (5467, 5468), // &NotGreaterGreat -> &NotGreaterGreate + new Transition (5471, 5472), // &NotGreaterL -> &NotGreaterLe + new Transition (5490, 5491), // &NotGreaterTild -> &NotGreaterTilde + new Transition (5528, 5529), // &NotL -> &NotLe + new Transition (5538, 5539), // &NotLeftTriangl -> &NotLeftTriangle + new Transition (5561, 5562), // &NotLessGr -> &NotLessGre + new Transition (5564, 5565), // &NotLessGreat -> &NotLessGreate + new Transition (5568, 5569), // &NotLessL -> &NotLessLe + new Transition (5587, 5588), // &NotLessTild -> &NotLessTilde + new Transition (5590, 5591), // &NotN -> &NotNe + new Transition (5593, 5594), // &NotNest -> &NotNeste + new Transition (5597, 5598), // &NotNestedGr -> &NotNestedGre + new Transition (5600, 5601), // &NotNestedGreat -> &NotNestedGreate + new Transition (5604, 5605), // &NotNestedGreaterGr -> &NotNestedGreaterGre + new Transition (5607, 5608), // &NotNestedGreaterGreat -> &NotNestedGreaterGreate + new Transition (5611, 5612), // &NotNestedL -> &NotNestedLe + new Transition (5615, 5616), // &NotNestedLessL -> &NotNestedLessLe + new Transition (5631, 5632), // &NotPr -> &NotPre + new Transition (5633, 5634), // &NotPrec -> &NotPrece + new Transition (5635, 5636), // &NotPreced -> &NotPrecede + new Transition (5656, 5657), // &NotR -> &NotRe + new Transition (5658, 5659), // &NotRev -> &NotReve + new Transition (5661, 5662), // &NotRevers -> &NotReverse + new Transition (5664, 5665), // &NotReverseEl -> &NotReverseEle + new Transition (5666, 5667), // &NotReverseElem -> &NotReverseEleme + new Transition (5681, 5682), // &NotRightTriangl -> &NotRightTriangle + new Transition (5698, 5699), // &NotSquar -> &NotSquare + new Transition (5703, 5704), // &NotSquareSubs -> &NotSquareSubse + new Transition (5713, 5714), // &NotSquareSup -> &NotSquareSupe + new Transition (5716, 5717), // &NotSquareSupers -> &NotSquareSuperse + new Transition (5728, 5729), // &NotSubs -> &NotSubse + new Transition (5739, 5740), // &NotSucc -> &NotSucce + new Transition (5740, 5741), // &NotSucce -> &NotSuccee + new Transition (5765, 5766), // &NotSucceedsTild -> &NotSucceedsTilde + new Transition (5768, 5769), // &NotSup -> &NotSupe + new Transition (5771, 5772), // &NotSupers -> &NotSuperse + new Transition (5784, 5785), // &NotTild -> &NotTilde + new Transition (5806, 5807), // &NotTildeTild -> &NotTildeTilde + new Transition (5809, 5810), // &NotV -> &NotVe + new Transition (5827, 5828), // &nparall -> &nparalle + new Transition (5842, 5848), // &npr -> &npre + new Transition (5845, 5846), // &nprcu -> &nprcue + new Transition (5850, 5852), // &nprec -> &nprece + new Transition (5891, 5893), // &nrtri -> &nrtrie + new Transition (5896, 5902), // &nsc -> &nsce + new Transition (5899, 5900), // &nsccu -> &nsccue + new Transition (5923, 5924), // &nshortparall -> &nshortparalle + new Transition (5928, 5930), // &nsim -> &nsime + new Transition (5945, 5946), // &nsqsub -> &nsqsube + new Transition (5948, 5949), // &nsqsup -> &nsqsupe + new Transition (5952, 5956), // &nsub -> &nsube + new Transition (5958, 5959), // &nsubs -> &nsubse + new Transition (5960, 5962), // &nsubset -> &nsubsete + new Transition (5968, 5970), // &nsucc -> &nsucce + new Transition (5973, 5977), // &nsup -> &nsupe + new Transition (5979, 5980), // &nsups -> &nsupse + new Transition (5981, 5983), // &nsupset -> &nsupsete + new Transition (5995, 5996), // &Ntild -> Ñ + new Transition (6000, 6001), // &ntild -> ñ + new Transition (6011, 6012), // &ntriangl -> &ntriangle + new Transition (6013, 6014), // &ntrianglel -> &ntrianglele + new Transition (6016, 6018), // &ntriangleleft -> &ntrianglelefte + new Transition (6025, 6027), // &ntriangleright -> &ntrianglerighte + new Transition (6034, 6036), // &num -> &nume + new Transition (6068, 6069), // &nvg -> &nvge + new Transition (6084, 6089), // &nvl -> &nvle + new Transition (6094, 6095), // &nvltri -> &nvltrie + new Transition (6104, 6105), // &nvrtri -> &nvrtrie + new Transition (6126, 6127), // &nwn -> &nwne + new Transition (6135, 6136), // &Oacut -> Ó + new Transition (6138, 6195), // &o -> &oe + new Transition (6142, 6143), // &oacut -> ó + new Transition (6217, 6218), // &Ograv -> Ò + new Transition (6222, 6223), // &ograv -> ò + new Transition (6253, 6254), // &olin -> &oline + new Transition (6258, 6268), // &Om -> &Ome + new Transition (6263, 6272), // &om -> &ome + new Transition (6302, 6332), // &op -> &ope + new Transition (6306, 6307), // &Op -> &Ope + new Transition (6318, 6319), // &OpenCurlyDoubl -> &OpenCurlyDouble + new Transition (6323, 6324), // &OpenCurlyDoubleQuot -> &OpenCurlyDoubleQuote + new Transition (6329, 6330), // &OpenCurlyQuot -> &OpenCurlyQuote + new Transition (6348, 6350), // &ord -> &orde + new Transition (6371, 6372), // &orslop -> &orslope + new Transition (6402, 6403), // &Otild -> Õ + new Transition (6408, 6409), // &otild -> õ + new Transition (6411, 6412), // &Otim -> &Otime + new Transition (6415, 6416), // &otim -> &otime + new Transition (6435, 6436), // &Ov -> &Ove + new Transition (6444, 6445), // &OverBrac -> &OverBrace + new Transition (6447, 6448), // &OverBrack -> &OverBracke + new Transition (6453, 6454), // &OverPar -> &OverPare + new Transition (6457, 6458), // &OverParenth -> &OverParenthe + new Transition (6463, 6497), // &p -> &pe + new Transition (6470, 6471), // ¶ll -> ¶lle + new Transition (6513, 6514), // &pert -> &perte + new Transition (6538, 6539), // &phon -> &phone + new Transition (6567, 6585), // &plus -> &pluse + new Transition (6614, 6615), // &Poincar -> &Poincare + new Transition (6619, 6620), // &Poincareplan -> &Poincareplane + new Transition (6640, 6672), // &Pr -> &Pre + new Transition (6642, 6653), // &pr -> &pre + new Transition (6648, 6649), // &prcu -> &prcue + new Transition (6655, 6702), // &prec -> &prece + new Transition (6668, 6669), // &preccurly -> &preccurlye + new Transition (6673, 6674), // &Prec -> &Prece + new Transition (6675, 6676), // &Preced -> &Precede + new Transition (6699, 6700), // &PrecedesTild -> &PrecedesTilde + new Transition (6705, 6713), // &precn -> &precne + new Transition (6726, 6727), // &Prim -> &Prime + new Transition (6730, 6731), // &prim -> &prime + new Transition (6762, 6763), // &proflin -> &profline + new Transition (6791, 6792), // &prur -> &prure + new Transition (6836, 6837), // &qprim -> &qprime + new Transition (6847, 6862), // &qu -> &que + new Transition (6849, 6850), // &quat -> &quate + new Transition (6864, 6866), // &quest -> &queste + new Transition (6876, 7074), // &r -> &re + new Transition (6882, 6901), // &ra -> &rae + new Transition (6883, 6884), // &rac -> &race + new Transition (6886, 7072), // &R -> &Re + new Transition (6890, 6891), // &Racut -> &Racute + new Transition (6894, 6895), // &racut -> &racute + new Transition (6912, 6916), // &rang -> &range + new Transition (6918, 6919), // &rangl -> &rangle + new Transition (7007, 7008), // &rbrac -> &rbrace + new Transition (7012, 7013), // &rbrk -> &rbrke + new Transition (7021, 7033), // &Rc -> &Rce + new Transition (7027, 7038), // &rc -> &rce + new Transition (7079, 7080), // &realin -> &realine + new Transition (7097, 7098), // &Rev -> &Reve + new Transition (7100, 7101), // &Revers -> &Reverse + new Transition (7103, 7104), // &ReverseEl -> &ReverseEle + new Transition (7105, 7106), // &ReverseElem -> &ReverseEleme + new Transition (7178, 7179), // &RightAngl -> &RightAngle + new Transition (7184, 7185), // &RightAngleBrack -> &RightAngleBracke + new Transition (7213, 7214), // &RightArrowL -> &RightArrowLe + new Transition (7228, 7229), // &RightC -> &RightCe + new Transition (7240, 7241), // &RightDoubl -> &RightDouble + new Transition (7246, 7247), // &RightDoubleBrack -> &RightDoubleBracke + new Transition (7252, 7253), // &RightDownT -> &RightDownTe + new Transition (7253, 7254), // &RightDownTe -> &RightDownTee + new Transition (7255, 7256), // &RightDownTeeV -> &RightDownTeeVe + new Transition (7262, 7263), // &RightDownV -> &RightDownVe + new Transition (7294, 7295), // &rightl -> &rightle + new Transition (7337, 7338), // &RightT -> &RightTe + new Transition (7338, 7339), // &RightTe -> &RightTee + new Transition (7347, 7348), // &RightTeeV -> &RightTeeVe + new Transition (7356, 7357), // &rightthr -> &rightthre + new Transition (7357, 7358), // &rightthre -> &rightthree + new Transition (7361, 7362), // &rightthreetim -> &rightthreetime + new Transition (7370, 7371), // &RightTriangl -> &RightTriangle + new Transition (7389, 7390), // &RightUpDownV -> &RightUpDownVe + new Transition (7396, 7397), // &RightUpT -> &RightUpTe + new Transition (7397, 7398), // &RightUpTe -> &RightUpTee + new Transition (7399, 7400), // &RightUpTeeV -> &RightUpTeeVe + new Transition (7406, 7407), // &RightUpV -> &RightUpVe + new Transition (7417, 7418), // &RightV -> &RightVe + new Transition (7438, 7439), // &risingdots -> &risingdotse + new Transition (7461, 7462), // &rmoustach -> &rmoustache + new Transition (7497, 7498), // &rotim -> &rotime + new Transition (7508, 7509), // &RoundImpli -> &RoundImplie + new Transition (7569, 7570), // &rthr -> &rthre + new Transition (7570, 7571), // &rthre -> &rthree + new Transition (7574, 7575), // &rtim -> &rtime + new Transition (7579, 7581), // &rtri -> &rtrie + new Transition (7591, 7592), // &Rul -> &Rule + new Transition (7593, 7594), // &RuleD -> &RuleDe + new Transition (7597, 7598), // &RuleDelay -> &RuleDelaye + new Transition (7614, 7615), // &Sacut -> &Sacute + new Transition (7617, 7703), // &s -> &se + new Transition (7621, 7622), // &sacut -> &sacute + new Transition (7629, 7653), // &Sc -> &Sce + new Transition (7631, 7651), // &sc -> &sce + new Transition (7646, 7647), // &sccu -> &sccue + new Transition (7697, 7701), // &sdot -> &sdote + new Transition (7786, 7787), // &ShortL -> &ShortLe + new Transition (7808, 7809), // &shortparall -> &shortparalle + new Transition (7847, 7853), // &sim -> &sime + new Transition (7865, 7866), // &simn -> &simne + new Transition (7891, 7892), // &SmallCircl -> &SmallCircle + new Transition (7894, 7911), // &sm -> &sme + new Transition (7898, 7899), // &smalls -> &smallse + new Transition (7921, 7922), // &smil -> &smile + new Transition (7924, 7926), // &smt -> &smte + new Transition (7958, 7959), // &spad -> &spade + new Transition (7986, 7988), // &sqsub -> &sqsube + new Transition (7990, 7991), // &sqsubs -> &sqsubse + new Transition (7992, 7994), // &sqsubset -> &sqsubsete + new Transition (7997, 7999), // &sqsup -> &sqsupe + new Transition (8001, 8002), // &sqsups -> &sqsupse + new Transition (8003, 8005), // &sqsupset -> &sqsupsete + new Transition (8012, 8013), // &Squar -> &Square + new Transition (8016, 8017), // &squar -> &square + new Transition (8021, 8022), // &SquareInt -> &SquareInte + new Transition (8024, 8025), // &SquareInters -> &SquareInterse + new Transition (8035, 8036), // &SquareSubs -> &SquareSubse + new Transition (8045, 8046), // &SquareSup -> &SquareSupe + new Transition (8048, 8049), // &SquareSupers -> &SquareSuperse + new Transition (8077, 8081), // &ss -> &sse + new Transition (8088, 8089), // &ssmil -> &ssmile + new Transition (8111, 8112), // &straight -> &straighte + new Transition (8131, 8139), // &sub -> &sube + new Transition (8150, 8153), // &subn -> &subne + new Transition (8165, 8166), // &Subs -> &Subse + new Transition (8169, 8170), // &subs -> &subse + new Transition (8171, 8173), // &subset -> &subsete + new Transition (8184, 8185), // &subsetn -> &subsetne + new Transition (8199, 8246), // &succ -> &succe + new Transition (8212, 8213), // &succcurly -> &succcurlye + new Transition (8217, 8218), // &Succ -> &Succe + new Transition (8218, 8219), // &Succe -> &Succee + new Transition (8243, 8244), // &SucceedsTild -> &SucceedsTilde + new Transition (8249, 8257), // &succn -> &succne + new Transition (8282, 8308), // &Sup -> &Supe + new Transition (8284, 8302), // &sup -> &supe + new Transition (8310, 8311), // &Supers -> &Superse + new Transition (8338, 8341), // &supn -> &supne + new Transition (8348, 8349), // &Sups -> &Supse + new Transition (8352, 8353), // &sups -> &supse + new Transition (8354, 8356), // &supset -> &supsete + new Transition (8361, 8362), // &supsetn -> &supsetne + new Transition (8404, 8449), // &t -> &te + new Transition (8407, 8408), // &targ -> &targe + new Transition (8419, 8431), // &Tc -> &Tce + new Transition (8425, 8436), // &tc -> &tce + new Transition (8451, 8452), // &telr -> &telre + new Transition (8461, 8462), // &th -> &the + new Transition (8463, 8464), // &ther -> &there + new Transition (8467, 8468), // &Th -> &The + new Transition (8469, 8470), // &Ther -> &There + new Transition (8473, 8474), // &Therefor -> &Therefore + new Transition (8478, 8479), // &therefor -> &therefore + new Transition (8513, 8514), // &ThickSpac -> &ThickSpace + new Transition (8524, 8525), // &ThinSpac -> &ThinSpace + new Transition (8546, 8547), // &Tild -> &Tilde + new Transition (8551, 8552), // &tild -> &tilde + new Transition (8573, 8574), // &TildeTild -> &TildeTilde + new Transition (8576, 8577), // &tim -> &time + new Transition (8590, 8591), // &to -> &toe + new Transition (8620, 8621), // &tprim -> &tprime + new Transition (8630, 8631), // &trad -> &trade + new Transition (8633, 8668), // &tri -> &trie + new Transition (8637, 8638), // &triangl -> &triangle + new Transition (8645, 8646), // &trianglel -> &trianglele + new Transition (8648, 8650), // &triangleleft -> &trianglelefte + new Transition (8659, 8661), // &triangleright -> &trianglerighte + new Transition (8679, 8680), // &Tripl -> &Triple + new Transition (8695, 8696), // &tritim -> &tritime + new Transition (8698, 8699), // &trp -> &trpe + new Transition (8743, 8744), // &twoh -> &twohe + new Transition (8747, 8748), // &twoheadl -> &twoheadle + new Transition (8772, 8773), // &Uacut -> Ú + new Transition (8779, 8780), // &uacut -> ú + new Transition (8798, 8807), // &Ubr -> &Ubre + new Transition (8803, 8811), // &ubr -> &ubre + new Transition (8808, 8809), // &Ubrev -> &Ubreve + new Transition (8812, 8813), // &ubrev -> &ubreve + new Transition (8863, 8864), // &Ugrav -> Ù + new Transition (8869, 8870), // &ugrav -> ù + new Transition (8891, 8893), // &ulcorn -> &ulcorne + new Transition (8917, 8918), // &Und -> &Unde + new Transition (8926, 8927), // &UnderBrac -> &UnderBrace + new Transition (8929, 8930), // &UnderBrack -> &UnderBracke + new Transition (8935, 8936), // &UnderPar -> &UnderPare + new Transition (8939, 8940), // &UnderParenth -> &UnderParenthe + new Transition (9053, 9054), // &upharpoonl -> &upharpoonle + new Transition (9068, 9069), // &Upp -> &Uppe + new Transition (9071, 9072), // &UpperL -> &UpperLe + new Transition (9108, 9109), // &UpT -> &UpTe + new Transition (9109, 9110), // &UpTe -> &UpTee + new Transition (9131, 9133), // &urcorn -> &urcorne + new Transition (9169, 9170), // &Utild -> &Utilde + new Transition (9174, 9175), // &utild -> &utilde + new Transition (9198, 9199), // &uwangl -> &uwangle + new Transition (9201, 9345), // &v -> &ve + new Transition (9208, 9209), // &var -> &vare + new Transition (9260, 9261), // &varsubs -> &varsubse + new Transition (9263, 9264), // &varsubsetn -> &varsubsetne + new Transition (9270, 9271), // &varsups -> &varsupse + new Transition (9273, 9274), // &varsupsetn -> &varsupsetne + new Transition (9280, 9281), // &varth -> &varthe + new Transition (9290, 9291), // &vartriangl -> &vartriangle + new Transition (9292, 9293), // &vartrianglel -> &vartrianglele + new Transition (9303, 9342), // &V -> &Ve + new Transition (9342, 9343), // &Ve -> &Vee + new Transition (9345, 9346), // &ve -> &vee + new Transition (9346, 9352), // &vee -> &veee + new Transition (9384, 9385), // &VerticalLin -> &VerticalLine + new Transition (9387, 9388), // &VerticalS -> &VerticalSe + new Transition (9400, 9401), // &VerticalTild -> &VerticalTilde + new Transition (9411, 9412), // &VeryThinSpac -> &VeryThinSpace + new Transition (9460, 9463), // &vsubn -> &vsubne + new Transition (9466, 9469), // &vsupn -> &vsupne + new Transition (9484, 9502), // &W -> &We + new Transition (9490, 9496), // &w -> &we + new Transition (9504, 9505), // &Wedg -> &Wedge + new Transition (9507, 9508), // &wedg -> &wedge + new Transition (9512, 9513), // &wei -> &weie + new Transition (9533, 9535), // &wr -> &wre + new Transition (9620, 9621), // &xotim -> &xotime + new Transition (9655, 9656), // &xv -> &xve + new Transition (9656, 9657), // &xve -> &xvee + new Transition (9659, 9660), // &xw -> &xwe + new Transition (9662, 9663), // &xwedg -> &xwedge + new Transition (9669, 9670), // &Yacut -> Ý + new Transition (9672, 9699), // &y -> &ye + new Transition (9676, 9677), // &yacut -> ý + new Transition (9747, 9791), // &Z -> &Ze + new Transition (9751, 9752), // &Zacut -> &Zacute + new Transition (9754, 9785), // &z -> &ze + new Transition (9758, 9759), // &zacut -> &zacute + new Transition (9785, 9786), // &ze -> &zee + new Transition (9802, 9803) // &ZeroWidthSpac -> &ZeroWidthSpace + }; + TransitionTable_f = new Transition[177] { + new Transition (0, 2503), // & -> &f + new Transition (1, 62), // &A -> &Af + new Transition (8, 60), // &a -> &af + new Transition (80, 81), // &ale -> &alef + new Transition (147, 158), // &angmsda -> &angmsdaf + new Transition (193, 194), // &Aop -> &Aopf + new Transition (196, 197), // &aop -> &aopf + new Transition (301, 439), // &b -> &bf + new Transition (331, 436), // &B -> &Bf + new Transition (553, 554), // &blacktrianglele -> &blacktrianglelef + new Transition (595, 596), // &Bop -> &Bopf + new Transition (599, 600), // &bop -> &bopf + new Transition (789, 950), // &C -> &Cf + new Transition (796, 953), // &c -> &cf + new Transition (833, 834), // &CapitalDi -> &CapitalDif + new Transition (834, 835), // &CapitalDif -> &CapitalDiff + new Transition (979, 1053), // &cir -> &cirf + new Transition (994, 995), // &circlearrowle -> &circlearrowlef + new Transition (1148, 1150), // &comp -> &compf + new Transition (1200, 1201), // &Cop -> &Copf + new Transition (1203, 1204), // &cop -> &copf + new Transition (1389, 1390), // &curvearrowle -> &curvearrowlef + new Transition (1425, 1541), // &D -> &Df + new Transition (1432, 1535), // &d -> &df + new Transition (1557, 1621), // &Di -> &Dif + new Transition (1621, 1622), // &Dif -> &Diff + new Transition (1686, 1687), // &Dop -> &Dopf + new Transition (1689, 1690), // &dop -> &dopf + new Transition (1777, 1778), // &DoubleLe -> &DoubleLef + new Transition (1805, 1806), // &DoubleLongLe -> &DoubleLongLef + new Transition (1940, 1941), // &downharpoonle -> &downharpoonlef + new Transition (1951, 1952), // &DownLe -> &DownLef + new Transition (2073, 2075), // &dtri -> &dtrif + new Transition (2108, 2180), // &E -> &Ef + new Transition (2115, 2175), // &e -> &ef + new Transition (2306, 2307), // &Eop -> &Eopf + new Transition (2309, 2310), // &eop -> &eopf + new Transition (2503, 2530), // &f -> &ff + new Transition (2517, 2544), // &F -> &Ff + new Transition (2605, 2606), // &fno -> &fnof + new Transition (2609, 2610), // &Fop -> &Fopf + new Transition (2613, 2614), // &fop -> &fopf + new Transition (2636, 2637), // &Fouriertr -> &Fouriertrf + new Transition (2701, 2802), // &g -> &gf + new Transition (2708, 2799), // &G -> &Gf + new Transition (2854, 2855), // &Gop -> &Gopf + new Transition (2858, 2859), // &gop -> &gopf + new Transition (3014, 3094), // &H -> &Hf + new Transition (3020, 3097), // &h -> &hf + new Transition (3027, 3028), // &hal -> &half + new Transition (3139, 3140), // &hookle -> &hooklef + new Transition (3160, 3161), // &Hop -> &Hopf + new Transition (3163, 3164), // &hop -> &hopf + new Transition (3236, 3284), // &I -> &If + new Transition (3243, 3281), // &i -> &if + new Transition (3281, 3282), // &if -> &iff + new Transition (3311, 3312), // &iin -> &iinf + new Transition (3365, 3366), // &imo -> &imof + new Transition (3378, 3385), // &in -> &inf + new Transition (3480, 3481), // &Iop -> &Iopf + new Transition (3483, 3484), // &iop -> &iopf + new Transition (3555, 3571), // &J -> &Jf + new Transition (3561, 3574), // &j -> &jf + new Transition (3583, 3584), // &Jop -> &Jopf + new Transition (3587, 3588), // &jop -> &jopf + new Transition (3618, 3648), // &K -> &Kf + new Transition (3624, 3651), // &k -> &kf + new Transition (3677, 3678), // &Kop -> &Kopf + new Transition (3681, 3682), // &kop -> &kopf + new Transition (3692, 4301), // &l -> &lf + new Transition (3698, 4312), // &L -> &Lf + new Transition (3752, 3753), // &Laplacetr -> &Laplacetrf + new Transition (3766, 3773), // &larr -> &larrf + new Transition (3768, 3770), // &larrb -> &larrbf + new Transition (3896, 3925), // &le -> &lef + new Transition (3898, 3899), // &Le -> &Lef + new Transition (4020, 4021), // &leftle -> &leftlef + new Transition (4361, 4362), // &Lle -> &Llef + new Transition (4438, 4439), // &LongLe -> &LongLef + new Transition (4448, 4449), // &Longle -> &Longlef + new Transition (4460, 4461), // &longle -> &longlef + new Transition (4550, 4551), // &looparrowle -> &looparrowlef + new Transition (4560, 4567), // &lop -> &lopf + new Transition (4564, 4565), // &Lop -> &Lopf + new Transition (4592, 4593), // &LowerLe -> &LowerLef + new Transition (4612, 4619), // &loz -> &lozf + new Transition (4732, 4736), // <ri -> <rif + new Transition (4767, 4865), // &m -> &mf + new Transition (4781, 4862), // &M -> &Mf + new Transition (4797, 4798), // &mapstole -> &mapstolef + new Transition (4859, 4860), // &Mellintr -> &Mellintrf + new Transition (4929, 4930), // &Mop -> &Mopf + new Transition (4932, 4933), // &mop -> &mopf + new Transition (4965, 5192), // &n -> &nf + new Transition (4971, 5189), // &N -> &Nf + new Transition (5270, 5282), // &nle -> &nlef + new Transition (5273, 5274), // &nLe -> &nLef + new Transition (5369, 5370), // &Nop -> &Nopf + new Transition (5373, 5374), // &nop -> &nopf + new Transition (5529, 5530), // &NotLe -> &NotLef + new Transition (6014, 6015), // &ntrianglele -> &ntrianglelef + new Transition (6079, 6080), // &nvin -> &nvinf + new Transition (6131, 6205), // &O -> &Of + new Transition (6138, 6200), // &o -> &of + new Transition (6295, 6296), // &Oop -> &Oopf + new Transition (6299, 6300), // &oop -> &oopf + new Transition (6348, 6356), // &ord -> ª + new Transition (6353, 6354), // &ordero -> &orderof + new Transition (6362, 6363), // &origo -> &origof + new Transition (6463, 6521), // &p -> &pf + new Transition (6482, 6518), // &P -> &Pf + new Transition (6547, 6548), // &pitch -> &pitchf + new Transition (6630, 6631), // &Pop -> &Popf + new Transition (6633, 6634), // &pop -> &popf + new Transition (6745, 6754), // &pro -> &prof + new Transition (6767, 6768), // &profsur -> &profsurf + new Transition (6813, 6814), // &Q -> &Qf + new Transition (6817, 6818), // &q -> &qf + new Transition (6826, 6827), // &Qop -> &Qopf + new Transition (6830, 6831), // &qop -> &qopf + new Transition (6876, 7135), // &r -> &rf + new Transition (6886, 7146), // &R -> &Rf + new Transition (6932, 6944), // &rarr -> &rarrf + new Transition (6937, 6939), // &rarrb -> &rarrbf + new Transition (7214, 7215), // &RightArrowLe -> &RightArrowLef + new Transition (7295, 7296), // &rightle -> &rightlef + new Transition (7481, 7489), // &rop -> &ropf + new Transition (7486, 7487), // &Rop -> &Ropf + new Transition (7579, 7583), // &rtri -> &rtrif + new Transition (7610, 7741), // &S -> &Sf + new Transition (7617, 7744), // &s -> &sf + new Transition (7787, 7788), // &ShortLe -> &ShortLef + new Transition (7841, 7843), // &sigma -> &sigmaf + new Transition (7936, 7937), // &so -> &sof + new Transition (7950, 7951), // &Sop -> &Sopf + new Transition (7953, 7954), // &sop -> &sopf + new Transition (8008, 8066), // &squ -> &squf + new Transition (8016, 8064), // &squar -> &squarf + new Transition (8093, 8094), // &sstar -> &sstarf + new Transition (8102, 8104), // &star -> &starf + new Transition (8400, 8455), // &T -> &Tf + new Transition (8404, 8458), // &t -> &tf + new Transition (8464, 8476), // &there -> &theref + new Transition (8470, 8471), // &There -> &Theref + new Transition (8594, 8608), // &top -> &topf + new Transition (8605, 8606), // &Top -> &Topf + new Transition (8646, 8647), // &trianglele -> &trianglelef + new Transition (8748, 8749), // &twoheadle -> &twoheadlef + new Transition (8768, 8855), // &U -> &Uf + new Transition (8775, 8849), // &u -> &uf + new Transition (8964, 8965), // &Uop -> &Uopf + new Transition (8967, 8968), // &uop -> &uopf + new Transition (9054, 9055), // &upharpoonle -> &upharpoonlef + new Transition (9072, 9073), // &UpperLe -> &UpperLef + new Transition (9178, 9180), // &utri -> &utrif + new Transition (9201, 9417), // &v -> &vf + new Transition (9293, 9294), // &vartrianglele -> &vartrianglelef + new Transition (9303, 9414), // &V -> &Vf + new Transition (9433, 9434), // &Vop -> &Vopf + new Transition (9437, 9438), // &vop -> &vopf + new Transition (9484, 9517), // &W -> &Wf + new Transition (9490, 9520), // &w -> &wf + new Transition (9524, 9525), // &Wop -> &Wopf + new Transition (9528, 9529), // &wop -> &wopf + new Transition (9548, 9569), // &x -> &xf + new Transition (9565, 9566), // &X -> &Xf + new Transition (9608, 9609), // &Xop -> &Xopf + new Transition (9611, 9612), // &xop -> &xopf + new Transition (9665, 9702), // &Y -> &Yf + new Transition (9672, 9705), // &y -> &yf + new Transition (9717, 9718), // &Yop -> &Yopf + new Transition (9721, 9722), // &yop -> &yopf + new Transition (9747, 9811), // &Z -> &Zf + new Transition (9754, 9814), // &z -> &zf + new Transition (9788, 9789), // &zeetr -> &zeetrf + new Transition (9833, 9834), // &Zop -> &Zopf + new Transition (9837, 9838) // &zop -> &zopf + }; + TransitionTable_g = new Transition[182] { + new Transition (0, 2701), // & -> &g + new Transition (1, 67), // &A -> &Ag + new Transition (8, 73), // &a -> &ag + new Transition (52, 53), // &AEli -> Æ + new Transition (57, 58), // &aeli -> æ + new Transition (108, 109), // &amal -> &amalg + new Transition (119, 136), // &an -> &ang + new Transition (147, 160), // &angmsda -> &angmsdag + new Transition (183, 184), // &Ao -> &Aog + new Transition (188, 189), // &ao -> &aog + new Transition (239, 240), // &Arin -> Å + new Transition (244, 245), // &arin -> å + new Transition (256, 257), // &Assi -> &Assig + new Transition (307, 308), // &backcon -> &backcong + new Transition (355, 357), // &barwed -> &barwedg + new Transition (371, 372), // &bcon -> &bcong + new Transition (442, 443), // &bi -> &big + new Transition (485, 486), // &bigtrian -> &bigtriang + new Transition (509, 510), // &bigwed -> &bigwedg + new Transition (527, 528), // &blacklozen -> &blacklozeng + new Transition (542, 543), // &blacktrian -> &blacktriang + new Transition (558, 559), // &blacktriangleri -> &blacktrianglerig + new Transition (999, 1000), // &circlearrowri -> &circlearrowrig + new Transition (1086, 1087), // &ClockwiseContourInte -> &ClockwiseContourInteg + new Transition (1164, 1165), // &con -> &cong + new Transition (1171, 1172), // &Con -> &Cong + new Transition (1194, 1195), // &ContourInte -> &ContourInteg + new Transition (1250, 1251), // &CounterClockwiseContourInte -> &CounterClockwiseContourInteg + new Transition (1373, 1374), // &curlywed -> &curlywedg + new Transition (1394, 1395), // &curvearrowri -> &curvearrowrig + new Transition (1426, 1427), // &Da -> &Dag + new Transition (1427, 1428), // &Dag -> &Dagg + new Transition (1433, 1434), // &da -> &dag + new Transition (1434, 1435), // &dag -> &dagg + new Transition (1494, 1495), // &dda -> &ddag + new Transition (1495, 1496), // &ddag -> &ddagg + new Transition (1516, 1517), // &de -> ° + new Transition (1599, 1633), // &di -> &dig + new Transition (1740, 1741), // &doublebarwed -> &doublebarwedg + new Transition (1758, 1759), // &DoubleContourInte -> &DoubleContourInteg + new Transition (1787, 1788), // &DoubleLeftRi -> &DoubleLeftRig + new Transition (1802, 1803), // &DoubleLon -> &DoubleLong + new Transition (1815, 1816), // &DoubleLongLeftRi -> &DoubleLongLeftRig + new Transition (1826, 1827), // &DoubleLongRi -> &DoubleLongRig + new Transition (1837, 1838), // &DoubleRi -> &DoubleRig + new Transition (1945, 1946), // &downharpoonri -> &downharpoonrig + new Transition (1955, 1956), // &DownLeftRi -> &DownLeftRig + new Transition (1988, 1989), // &DownRi -> &DownRig + new Transition (2088, 2089), // &dwan -> &dwang + new Transition (2101, 2102), // &dzi -> &dzig + new Transition (2108, 2187), // &E -> &Eg + new Transition (2115, 2185), // &e -> &eg + new Transition (2290, 2291), // &en -> &eng + new Transition (2296, 2297), // &Eo -> &Eog + new Transition (2301, 2302), // &eo -> &eog + new Transition (2357, 2358), // &eqslant -> &eqslantg + new Transition (2508, 2509), // &fallin -> &falling + new Transition (2533, 2534), // &ffili -> &ffilig + new Transition (2537, 2538), // &ffli -> &fflig + new Transition (2541, 2542), // &fflli -> &ffllig + new Transition (2551, 2552), // &fili -> &filig + new Transition (2589, 2590), // &fjli -> &fjlig + new Transition (2597, 2598), // &flli -> &fllig + new Transition (2701, 2807), // &g -> &gg + new Transition (2708, 2805), // &G -> &Gg + new Transition (2807, 2809), // &gg -> &ggg + new Transition (3149, 3150), // &hookri -> &hookrig + new Transition (3236, 3289), // &I -> &Ig + new Transition (3243, 3295), // &i -> &ig + new Transition (3322, 3323), // &IJli -> &IJlig + new Transition (3327, 3328), // &ijli -> &ijlig + new Transition (3332, 3344), // &Ima -> &Imag + new Transition (3337, 3341), // &ima -> &imag + new Transition (3407, 3408), // &inte -> &integ + new Transition (3413, 3414), // &Inte -> &Integ + new Transition (3467, 3476), // &io -> &iog + new Transition (3471, 3472), // &Io -> &Iog + new Transition (3624, 3654), // &k -> &kg + new Transition (3692, 4317), // &l -> &lg + new Transition (3705, 3718), // &la -> &lag + new Transition (3733, 3734), // &Lan -> &Lang + new Transition (3736, 3737), // &lan -> &lang + new Transition (3894, 4183), // &lE -> &lEg + new Transition (3896, 4185), // &le -> &leg + new Transition (3902, 3903), // &LeftAn -> &LeftAng + new Transition (3938, 3939), // &LeftArrowRi -> &LeftArrowRig + new Transition (3958, 3959), // &LeftCeilin -> &LeftCeiling + new Transition (4031, 4032), // &LeftRi -> &LeftRig + new Transition (4042, 4043), // &Leftri -> &Leftrig + new Transition (4053, 4054), // &leftri -> &leftrig + new Transition (4077, 4078), // &leftrightsqui -> &leftrightsquig + new Transition (4123, 4124), // &LeftTrian -> &LeftTriang + new Transition (4197, 4210), // &les -> &lesg + new Transition (4215, 4271), // &less -> &lessg + new Transition (4228, 4229), // &lesseq -> &lesseqg + new Transition (4233, 4234), // &lesseqq -> &lesseqqg + new Transition (4424, 4425), // &loan -> &loang + new Transition (4435, 4436), // &Lon -> &Long + new Transition (4457, 4458), // &lon -> &long + new Transition (4470, 4471), // &LongLeftRi -> &LongLeftRig + new Transition (4481, 4482), // &Longleftri -> &Longleftrig + new Transition (4492, 4493), // &longleftri -> &longleftrig + new Transition (4510, 4511), // &LongRi -> &LongRig + new Transition (4521, 4522), // &Longri -> &Longrig + new Transition (4532, 4533), // &longri -> &longrig + new Transition (4555, 4556), // &looparrowri -> &looparrowrig + new Transition (4602, 4603), // &LowerRi -> &LowerRig + new Transition (4615, 4616), // &lozen -> &lozeng + new Transition (4670, 4674), // &lsim -> &lsimg + new Transition (4838, 4839), // &measuredan -> &measuredang + new Transition (4965, 5195), // &n -> &ng + new Transition (4983, 4984), // &nan -> &nang + new Transition (5045, 5046), // &ncon -> &ncong + new Transition (5084, 5085), // &Ne -> &Neg + new Transition (5212, 5213), // &nG -> &nGg + new Transition (5291, 5292), // &nLeftri -> &nLeftrig + new Transition (5302, 5303), // &nleftri -> &nleftrig + new Transition (5361, 5362), // &NonBreakin -> &NonBreaking + new Transition (5382, 5383), // &NotCon -> &NotCong + new Transition (5536, 5537), // &NotLeftTrian -> &NotLeftTriang + new Transition (5671, 5672), // &NotRi -> &NotRig + new Transition (5679, 5680), // &NotRightTrian -> &NotRightTriang + new Transition (5869, 5870), // &nRi -> &nRig + new Transition (5879, 5880), // &nri -> &nrig + new Transition (5988, 5989), // &nt -> &ntg + new Transition (6003, 6004), // &ntl -> &ntlg + new Transition (6009, 6010), // &ntrian -> &ntriang + new Transition (6022, 6023), // &ntriangleri -> &ntrianglerig + new Transition (6043, 6068), // &nv -> &nvg + new Transition (6131, 6214), // &O -> &Og + new Transition (6138, 6210), // &o -> &og + new Transition (6192, 6193), // &OEli -> &OElig + new Transition (6197, 6198), // &oeli -> &oelig + new Transition (6268, 6269), // &Ome -> &Omeg + new Transition (6272, 6273), // &ome -> &omeg + new Transition (6360, 6361), // &ori -> &orig + new Transition (6908, 6909), // &Ran -> &Rang + new Transition (6911, 6912), // &ran -> &rang + new Transition (7074, 7095), // &re -> ® + new Transition (7171, 7172), // &Ri -> &Rig + new Transition (7176, 7177), // &RightAn -> &RightAng + new Transition (7199, 7200), // &ri -> &rig + new Transition (7233, 7234), // &RightCeilin -> &RightCeiling + new Transition (7315, 7316), // &rightri -> &rightrig + new Transition (7329, 7330), // &rightsqui -> &rightsquig + new Transition (7368, 7369), // &RightTrian -> &RightTriang + new Transition (7428, 7429), // &rin -> &ring + new Transition (7433, 7434), // &risin -> &rising + new Transition (7471, 7472), // &roan -> &roang + new Transition (7514, 7516), // &rpar -> &rparg + new Transition (7532, 7533), // &Rri -> &Rrig + new Transition (7813, 7814), // &ShortRi -> &ShortRig + new Transition (7833, 7834), // &Si -> &Sig + new Transition (7838, 7839), // &si -> &sig + new Transition (7847, 7857), // &sim -> &simg + new Transition (8108, 8109), // &strai -> &straig + new Transition (8279, 8280), // &sun -> &sung + new Transition (8397, 8398), // &szli -> ß + new Transition (8406, 8407), // &tar -> &targ + new Transition (8635, 8636), // &trian -> &triang + new Transition (8656, 8657), // &triangleri -> &trianglerig + new Transition (8758, 8759), // &twoheadri -> &twoheadrig + new Transition (8768, 8860), // &U -> &Ug + new Transition (8775, 8866), // &u -> &ug + new Transition (8954, 8955), // &Uo -> &Uog + new Transition (8959, 8960), // &uo -> &uog + new Transition (9059, 9060), // &upharpoonri -> &upharpoonrig + new Transition (9082, 9083), // &UpperRi -> &UpperRig + new Transition (9142, 9143), // &Urin -> &Uring + new Transition (9146, 9147), // &urin -> &uring + new Transition (9196, 9197), // &uwan -> &uwang + new Transition (9203, 9204), // &van -> &vang + new Transition (9228, 9229), // &varnothin -> &varnothing + new Transition (9253, 9254), // &varsi -> &varsig + new Transition (9288, 9289), // &vartrian -> &vartriang + new Transition (9298, 9299), // &vartriangleri -> &vartrianglerig + new Transition (9478, 9479), // &vzi -> &vzig + new Transition (9481, 9482), // &vzigza -> &vzigzag + new Transition (9497, 9507), // &wed -> &wedg + new Transition (9503, 9504), // &Wed -> &Wedg + new Transition (9661, 9662), // &xwed -> &xwedg + new Transition (9825, 9826) // &zi -> &zig + }; + TransitionTable_h = new Transition[159] { + new Transition (0, 3020), // & -> &h + new Transition (86, 87), // &alep -> &aleph + new Transition (90, 91), // &Alp -> &Alph + new Transition (94, 95), // &alp -> &alph + new Transition (147, 162), // &angmsda -> &angmsdah + new Transition (173, 174), // &angsp -> &angsph + new Transition (338, 339), // &Backslas -> &Backslash + new Transition (426, 429), // &bet -> &beth + new Transition (559, 560), // &blacktrianglerig -> &blacktrianglerigh + new Transition (613, 638), // &box -> &boxh + new Transition (691, 697), // &boxV -> &boxVh + new Transition (693, 701), // &boxv -> &boxvh + new Transition (758, 762), // &bsol -> &bsolh + new Transition (789, 973), // &C -> &Ch + new Transition (796, 960), // &c -> &ch + new Transition (1000, 1001), // &circlearrowrig -> &circlearrowrigh + new Transition (1016, 1017), // &circleddas -> &circleddash + new Transition (1395, 1396), // &curvearrowrig -> &curvearrowrigh + new Transition (1432, 1550), // &d -> &dh + new Transition (1441, 1442), // &dalet -> &daleth + new Transition (1454, 1455), // &das -> &dash + new Transition (1457, 1458), // &Das -> &Dash + new Transition (1506, 1507), // &DDotra -> &DDotrah + new Transition (1537, 1538), // &dfis -> &dfish + new Transition (1788, 1789), // &DoubleLeftRig -> &DoubleLeftRigh + new Transition (1816, 1817), // &DoubleLongLeftRig -> &DoubleLongLeftRigh + new Transition (1827, 1828), // &DoubleLongRig -> &DoubleLongRigh + new Transition (1838, 1839), // &DoubleRig -> &DoubleRigh + new Transition (1896, 1932), // &down -> &downh + new Transition (1946, 1947), // &downharpoonrig -> &downharpoonrigh + new Transition (1956, 1957), // &DownLeftRig -> &DownLeftRigh + new Transition (1989, 1990), // &DownRig -> &DownRigh + new Transition (2077, 2082), // &du -> &duh + new Transition (2439, 2445), // &et -> ð + new Transition (3132, 3133), // &homt -> &homth + new Transition (3150, 3151), // &hookrig -> &hookrigh + new Transition (3194, 3195), // &hslas -> &hslash + new Transition (3231, 3232), // &hyp -> &hyph + new Transition (3362, 3363), // &imat -> &imath + new Transition (3435, 3436), // &intlar -> &intlarh + new Transition (3579, 3580), // &jmat -> &jmath + new Transition (3624, 3664), // &k -> &kh + new Transition (3692, 4325), // &l -> &lh + new Transition (3766, 3776), // &larr -> &larrh + new Transition (3880, 3881), // &ldrd -> &ldrdh + new Transition (3886, 3887), // &ldrus -> &ldrush + new Transition (3891, 3892), // &lds -> &ldsh + new Transition (3926, 4004), // &left -> &lefth + new Transition (3939, 3940), // &LeftArrowRig -> &LeftArrowRigh + new Transition (4032, 4033), // &LeftRig -> &LeftRigh + new Transition (4043, 4044), // &Leftrig -> &Leftrigh + new Transition (4054, 4055), // &leftrig -> &leftrigh + new Transition (4056, 4065), // &leftright -> &leftrighth + new Transition (4109, 4110), // &leftt -> &leftth + new Transition (4303, 4304), // &lfis -> &lfish + new Transition (4348, 4370), // &ll -> &llh + new Transition (4397, 4398), // &lmoustac -> &lmoustach + new Transition (4471, 4472), // &LongLeftRig -> &LongLeftRigh + new Transition (4482, 4483), // &Longleftrig -> &Longleftrigh + new Transition (4493, 4494), // &longleftrig -> &longleftrigh + new Transition (4511, 4512), // &LongRig -> &LongRigh + new Transition (4522, 4523), // &Longrig -> &Longrigh + new Transition (4533, 4534), // &longrig -> &longrigh + new Transition (4556, 4557), // &looparrowrig -> &looparrowrigh + new Transition (4603, 4604), // &LowerRig -> &LowerRigh + new Transition (4628, 4640), // &lr -> &lrh + new Transition (4652, 4667), // &ls -> &lsh + new Transition (4658, 4665), // &Ls -> &Lsh + new Transition (4698, 4710), // < -> <h + new Transition (4745, 4746), // &lurds -> &lurdsh + new Transition (4750, 4751), // &luru -> &luruh + new Transition (4767, 4868), // &m -> &mh + new Transition (4822, 4823), // &mdas -> &mdash + new Transition (4965, 5227), // &n -> &nh + new Transition (5061, 5062), // &ndas -> &ndash + new Transition (5067, 5068), // &near -> &nearh + new Transition (5103, 5104), // &NegativeT -> &NegativeTh + new Transition (5125, 5126), // &NegativeVeryT -> &NegativeVeryTh + new Transition (5292, 5293), // &nLeftrig -> &nLeftrigh + new Transition (5303, 5304), // &nleftrig -> &nleftrigh + new Transition (5672, 5673), // &NotRig -> &NotRigh + new Transition (5870, 5871), // &nRig -> &nRigh + new Transition (5880, 5881), // &nrig -> &nrigh + new Transition (5895, 5910), // &ns -> &nsh + new Transition (6023, 6024), // &ntrianglerig -> &ntrianglerigh + new Transition (6050, 6051), // &nVDas -> &nVDash + new Transition (6055, 6056), // &nVdas -> &nVdash + new Transition (6060, 6061), // &nvDas -> &nvDash + new Transition (6065, 6066), // &nvdas -> &nvdash + new Transition (6113, 6114), // &nwar -> &nwarh + new Transition (6138, 6227), // &o -> &oh + new Transition (6165, 6166), // &odas -> &odash + new Transition (6388, 6389), // &Oslas -> Ø + new Transition (6393, 6394), // &oslas -> ø + new Transition (6456, 6457), // &OverParent -> &OverParenth + new Transition (6463, 6527), // &p -> &ph + new Transition (6482, 6524), // &P -> &Ph + new Transition (6546, 6547), // &pitc -> &pitch + new Transition (6559, 6561), // &planck -> &planckh + new Transition (6876, 7155), // &r -> &rh + new Transition (6886, 7164), // &R -> &Rh + new Transition (6932, 6947), // &rarr -> &rarrh + new Transition (7058, 7059), // &rdld -> &rdldh + new Transition (7069, 7070), // &rds -> &rdsh + new Transition (7137, 7138), // &rfis -> &rfish + new Transition (7172, 7173), // &Rig -> &Righ + new Transition (7200, 7201), // &rig -> &righ + new Transition (7202, 7279), // &right -> &righth + new Transition (7297, 7305), // &rightleft -> &rightlefth + new Transition (7316, 7317), // &rightrig -> &rightrigh + new Transition (7354, 7355), // &rightt -> &rightth + new Transition (7442, 7447), // &rl -> &rlh + new Transition (7460, 7461), // &rmoustac -> &rmoustach + new Transition (7533, 7534), // &Rrig -> &Rrigh + new Transition (7542, 7557), // &rs -> &rsh + new Transition (7548, 7555), // &Rs -> &Rsh + new Transition (7567, 7568), // &rt -> &rth + new Transition (7603, 7604), // &rulu -> &ruluh + new Transition (7610, 7772), // &S -> &Sh + new Transition (7617, 7751), // &s -> &sh + new Transition (7705, 7706), // &sear -> &searh + new Transition (7762, 7763), // &shc -> &shch + new Transition (7814, 7815), // &ShortRig -> &ShortRigh + new Transition (7907, 7908), // &smas -> &smash + new Transition (8109, 8110), // &straig -> &straigh + new Transition (8120, 8121), // &straightp -> &straightph + new Transition (8216, 8269), // &Suc -> &Such + new Transition (8270, 8271), // &SuchT -> &SuchTh + new Transition (8284, 8320), // &sup -> &suph + new Transition (8377, 8378), // &swar -> &swarh + new Transition (8400, 8467), // &T -> &Th + new Transition (8404, 8461), // &t -> &th + new Transition (8657, 8658), // &trianglerig -> &trianglerigh + new Transition (8709, 8723), // &ts -> &tsh + new Transition (8742, 8743), // &two -> &twoh + new Transition (8759, 8760), // &twoheadrig -> &twoheadrigh + new Transition (8775, 8876), // &u -> &uh + new Transition (8829, 8845), // &ud -> &udh + new Transition (8851, 8852), // &ufis -> &ufish + new Transition (8938, 8939), // &UnderParent -> &UnderParenth + new Transition (8983, 9046), // &up -> &uph + new Transition (9060, 9061), // &upharpoonrig -> &upharpoonrigh + new Transition (9083, 9084), // &UpperRig -> &UpperRigh + new Transition (9096, 9098), // &upsi -> &upsih + new Transition (9225, 9226), // &varnot -> &varnoth + new Transition (9231, 9232), // &varp -> &varph + new Transition (9247, 9249), // &varr -> &varrh + new Transition (9279, 9280), // &vart -> &varth + new Transition (9299, 9300), // &vartrianglerig -> &vartrianglerigh + new Transition (9322, 9323), // &VDas -> &VDash + new Transition (9327, 9328), // &Vdas -> &Vdash + new Transition (9332, 9333), // &vDas -> &vDash + new Transition (9337, 9338), // &vdas -> &vdash + new Transition (9404, 9405), // &VeryT -> &VeryTh + new Transition (9474, 9475), // &Vvdas -> &Vvdash + new Transition (9537, 9538), // &wreat -> &wreath + new Transition (9548, 9572), // &x -> &xh + new Transition (9754, 9821), // &z -> &zh + new Transition (9797, 9798) // &ZeroWidt -> &ZeroWidth + }; + TransitionTable_i = new Transition[428] { + new Transition (0, 3243), // & -> &i + new Transition (27, 38), // &ac -> &aci + new Transition (33, 34), // &Ac -> &Aci + new Transition (51, 52), // &AEl -> &AEli + new Transition (56, 57), // &ael -> &aeli + new Transition (199, 210), // &ap -> &api + new Transition (202, 203), // &apac -> &apaci + new Transition (224, 225), // &ApplyFunct -> &ApplyFuncti + new Transition (237, 238), // &Ar -> &Ari + new Transition (242, 243), // &ar -> &ari + new Transition (255, 256), // &Ass -> &Assi + new Transition (269, 270), // &At -> &Ati + new Transition (275, 276), // &at -> &ati + new Transition (289, 297), // &aw -> &awi + new Transition (292, 293), // &awcon -> &awconi + new Transition (301, 442), // &b -> &bi + new Transition (312, 313), // &backeps -> &backepsi + new Transition (319, 320), // &backpr -> &backpri + new Transition (324, 325), // &backs -> &backsi + new Transition (406, 407), // &beps -> &bepsi + new Transition (419, 420), // &Bernoull -> &Bernoulli + new Transition (444, 448), // &bigc -> &bigci + new Transition (465, 466), // &bigot -> &bigoti + new Transition (482, 483), // &bigtr -> &bigtri + new Transition (539, 540), // &blacktr -> &blacktri + new Transition (557, 558), // &blacktriangler -> &blacktriangleri + new Transition (583, 584), // &bnequ -> &bnequi + new Transition (609, 610), // &bowt -> &bowti + new Transition (656, 657), // &boxm -> &boxmi + new Transition (667, 668), // &boxt -> &boxti + new Transition (720, 721), // &bpr -> &bpri + new Transition (744, 752), // &bs -> &bsi + new Transition (749, 750), // &bsem -> &bsemi + new Transition (789, 1019), // &C -> &Ci + new Transition (796, 978), // &c -> &ci + new Transition (803, 828), // &Cap -> &Capi + new Transition (832, 833), // &CapitalD -> &CapitalDi + new Transition (840, 841), // &CapitalDifferent -> &CapitalDifferenti + new Transition (861, 890), // &cc -> &cci + new Transition (866, 886), // &Cc -> &Cci + new Transition (877, 878), // &Cced -> &Ccedi + new Transition (882, 883), // &cced -> &ccedi + new Transition (895, 896), // &Ccon -> &Cconi + new Transition (916, 917), // &ced -> &cedi + new Transition (921, 922), // &Ced -> &Cedi + new Transition (960, 976), // &ch -> &chi + new Transition (973, 974), // &Ch -> &Chi + new Transition (998, 999), // &circlearrowr -> &circlearrowri + new Transition (1009, 1010), // &circledc -> &circledci + new Transition (1032, 1033), // &CircleM -> &CircleMi + new Transition (1043, 1044), // &CircleT -> &CircleTi + new Transition (1054, 1055), // &cirfn -> &cirfni + new Transition (1059, 1060), // &cirm -> &cirmi + new Transition (1064, 1065), // &cirsc -> &cirsci + new Transition (1072, 1073), // &Clockw -> &Clockwi + new Transition (1122, 1123), // &clubsu -> &clubsui + new Transition (1164, 1183), // &con -> &coni + new Transition (1171, 1179), // &Con -> &Coni + new Transition (1236, 1237), // &CounterClockw -> &CounterClockwi + new Transition (1393, 1394), // &curvearrowr -> &curvearrowri + new Transition (1407, 1415), // &cw -> &cwi + new Transition (1410, 1411), // &cwcon -> &cwconi + new Transition (1425, 1557), // &D -> &Di + new Transition (1432, 1599), // &d -> &di + new Transition (1535, 1536), // &df -> &dfi + new Transition (1560, 1561), // &Diacr -> &Diacri + new Transition (1562, 1563), // &Diacrit -> &Diacriti + new Transition (1593, 1594), // &DiacriticalT -> &DiacriticalTi + new Transition (1613, 1614), // &diamondsu -> &diamondsui + new Transition (1627, 1628), // &Different -> &Differenti + new Transition (1639, 1640), // &dis -> &disi + new Transition (1643, 1645), // &div -> &divi + new Transition (1651, 1652), // ÷ont -> ÷onti + new Transition (1713, 1714), // &dotm -> &dotmi + new Transition (1786, 1787), // &DoubleLeftR -> &DoubleLeftRi + new Transition (1814, 1815), // &DoubleLongLeftR -> &DoubleLongLeftRi + new Transition (1825, 1826), // &DoubleLongR -> &DoubleLongRi + new Transition (1836, 1837), // &DoubleR -> &DoubleRi + new Transition (1872, 1873), // &DoubleVert -> &DoubleVerti + new Transition (1944, 1945), // &downharpoonr -> &downharpoonri + new Transition (1954, 1955), // &DownLeftR -> &DownLeftRi + new Transition (1987, 1988), // &DownR -> &DownRi + new Transition (2072, 2073), // &dtr -> &dtri + new Transition (2097, 2101), // &dz -> &dzi + new Transition (2127, 2142), // &Ec -> &Eci + new Transition (2133, 2139), // &ec -> &eci + new Transition (2204, 2213), // &el -> &eli + new Transition (2323, 2324), // &eps -> &epsi + new Transition (2327, 2328), // &Eps -> &Epsi + new Transition (2340, 2341), // &eqc -> &eqci + new Transition (2350, 2351), // &eqs -> &eqsi + new Transition (2368, 2387), // &Equ -> &Equi + new Transition (2372, 2396), // &equ -> &equi + new Transition (2377, 2378), // &EqualT -> &EqualTi + new Transition (2388, 2389), // &Equil -> &Equili + new Transition (2391, 2392), // &Equilibr -> &Equilibri + new Transition (2418, 2430), // &Es -> &Esi + new Transition (2422, 2433), // &es -> &esi + new Transition (2458, 2462), // &ex -> &exi + new Transition (2466, 2467), // &Ex -> &Exi + new Transition (2477, 2478), // &expectat -> &expectati + new Transition (2487, 2488), // &Exponent -> &Exponenti + new Transition (2497, 2498), // &exponent -> &exponenti + new Transition (2503, 2549), // &f -> &fi + new Transition (2506, 2507), // &fall -> &falli + new Transition (2517, 2554), // &F -> &Fi + new Transition (2530, 2531), // &ff -> &ffi + new Transition (2532, 2533), // &ffil -> &ffili + new Transition (2536, 2537), // &ffl -> &ffli + new Transition (2540, 2541), // &ffll -> &fflli + new Transition (2550, 2551), // &fil -> &fili + new Transition (2588, 2589), // &fjl -> &fjli + new Transition (2596, 2597), // &fll -> &flli + new Transition (2631, 2632), // &Four -> &Fouri + new Transition (2642, 2643), // &fpart -> &fparti + new Transition (2701, 2811), // &g -> &gi + new Transition (2736, 2742), // &Gc -> &Gci + new Transition (2738, 2739), // &Gced -> &Gcedi + new Transition (2746, 2747), // &gc -> &gci + new Transition (2849, 2850), // &gns -> &gnsi + new Transition (2917, 2918), // &GreaterT -> &GreaterTi + new Transition (2927, 2931), // &gs -> &gsi + new Transition (2944, 2947), // >c -> >ci + new Transition (2998, 2999), // >rs -> >rsi + new Transition (3014, 3100), // &H -> &Hi + new Transition (3021, 3022), // &ha -> &hai + new Transition (3030, 3031), // &ham -> &hami + new Transition (3052, 3053), // &harrc -> &harrci + new Transition (3064, 3065), // &Hc -> &Hci + new Transition (3069, 3070), // &hc -> &hci + new Transition (3080, 3081), // &heartsu -> &heartsui + new Transition (3085, 3086), // &hell -> &helli + new Transition (3148, 3149), // &hookr -> &hookri + new Transition (3171, 3172), // &Hor -> &Hori + new Transition (3179, 3180), // &HorizontalL -> &HorizontalLi + new Transition (3243, 3301), // &i -> &ii + new Transition (3250, 3257), // &ic -> &ici + new Transition (3252, 3253), // &Ic -> &Ici + new Transition (3301, 3303), // &ii -> &iii + new Transition (3303, 3304), // &iii -> &iiii + new Transition (3312, 3313), // &iinf -> &iinfi + new Transition (3321, 3322), // &IJl -> &IJli + new Transition (3326, 3327), // &ijl -> &ijli + new Transition (3344, 3345), // &Imag -> &Imagi + new Transition (3352, 3353), // &imagl -> &imagli + new Transition (3373, 3374), // &Impl -> &Impli + new Transition (3385, 3386), // &inf -> &infi + new Transition (3389, 3390), // &infint -> &infinti + new Transition (3428, 3429), // &Intersect -> &Intersecti + new Transition (3444, 3445), // &Inv -> &Invi + new Transition (3446, 3447), // &Invis -> &Invisi + new Transition (3457, 3458), // &InvisibleT -> &InvisibleTi + new Transition (3507, 3511), // &is -> &isi + new Transition (3526, 3534), // &it -> &iti + new Transition (3528, 3529), // &It -> &Iti + new Transition (3556, 3557), // &Jc -> &Jci + new Transition (3562, 3563), // &jc -> &jci + new Transition (3634, 3635), // &Kced -> &Kcedi + new Transition (3640, 3641), // &kced -> &kcedi + new Transition (3785, 3786), // &larrs -> &larrsi + new Transition (3795, 3796), // &lAta -> &lAtai + new Transition (3799, 3800), // &lata -> &latai + new Transition (3850, 3851), // &Lced -> &Lcedi + new Transition (3854, 3859), // &lce -> &lcei + new Transition (3855, 3856), // &lced -> &lcedi + new Transition (3937, 3938), // &LeftArrowR -> &LeftArrowRi + new Transition (3949, 3950), // &leftarrowta -> &leftarrowtai + new Transition (3954, 3955), // &LeftCe -> &LeftCei + new Transition (3956, 3957), // &LeftCeil -> &LeftCeili + new Transition (4030, 4031), // &LeftR -> &LeftRi + new Transition (4041, 4042), // &Leftr -> &Leftri + new Transition (4052, 4053), // &leftr -> &leftri + new Transition (4076, 4077), // &leftrightsqu -> &leftrightsqui + new Transition (4114, 4115), // &leftthreet -> &leftthreeti + new Transition (4120, 4121), // &LeftTr -> &LeftTri + new Transition (4280, 4281), // &lesss -> &lesssi + new Transition (4295, 4296), // &LessT -> &LessTi + new Transition (4301, 4302), // &lf -> &lfi + new Transition (4376, 4377), // &lltr -> &lltri + new Transition (4379, 4380), // &Lm -> &Lmi + new Transition (4385, 4386), // &lm -> &lmi + new Transition (4418, 4419), // &lns -> &lnsi + new Transition (4469, 4470), // &LongLeftR -> &LongLeftRi + new Transition (4480, 4481), // &Longleftr -> &Longleftri + new Transition (4491, 4492), // &longleftr -> &longleftri + new Transition (4509, 4510), // &LongR -> &LongRi + new Transition (4520, 4521), // &Longr -> &Longri + new Transition (4531, 4532), // &longr -> &longri + new Transition (4554, 4555), // &looparrowr -> &looparrowri + new Transition (4573, 4574), // &lot -> &loti + new Transition (4601, 4602), // &LowerR -> &LowerRi + new Transition (4649, 4650), // &lrtr -> &lrtri + new Transition (4652, 4669), // &ls -> &lsi + new Transition (4698, 4715), // < -> <i + new Transition (4700, 4703), // <c -> <ci + new Transition (4731, 4732), // <r -> <ri + new Transition (4767, 4871), // &m -> &mi + new Transition (4781, 4900), // &M -> &Mi + new Transition (4844, 4845), // &Med -> &Medi + new Transition (4855, 4856), // &Mell -> &Melli + new Transition (4882, 4883), // &midc -> &midci + new Transition (4955, 4956), // &mult -> &multi + new Transition (4965, 5240), // &n -> &ni + new Transition (4986, 4990), // &nap -> &napi + new Transition (5035, 5036), // &Nced -> &Ncedi + new Transition (5040, 5041), // &nced -> &ncedi + new Transition (5087, 5088), // &Negat -> &Negati + new Transition (5093, 5094), // &NegativeMed -> &NegativeMedi + new Transition (5104, 5105), // &NegativeTh -> &NegativeThi + new Transition (5126, 5127), // &NegativeVeryTh -> &NegativeVeryThi + new Transition (5136, 5137), // &nequ -> &nequi + new Transition (5140, 5145), // &nes -> &nesi + new Transition (5177, 5178), // &NewL -> &NewLi + new Transition (5182, 5183), // &nex -> &nexi + new Transition (5215, 5216), // &ngs -> &ngsi + new Transition (5290, 5291), // &nLeftr -> &nLeftri + new Transition (5301, 5302), // &nleftr -> &nleftri + new Transition (5328, 5329), // &nls -> &nlsi + new Transition (5336, 5337), // &nltr -> &nltri + new Transition (5343, 5344), // &nm -> &nmi + new Transition (5359, 5360), // &NonBreak -> &NonBreaki + new Transition (5378, 5512), // ¬ -> ¬i + new Transition (5405, 5406), // &NotDoubleVert -> &NotDoubleVerti + new Transition (5427, 5428), // &NotEqualT -> &NotEqualTi + new Transition (5433, 5434), // &NotEx -> &NotExi + new Transition (5487, 5488), // &NotGreaterT -> &NotGreaterTi + new Transition (5533, 5534), // &NotLeftTr -> &NotLeftTri + new Transition (5584, 5585), // &NotLessT -> &NotLessTi + new Transition (5620, 5621), // ¬n -> ¬ni + new Transition (5656, 5671), // &NotR -> &NotRi + new Transition (5676, 5677), // &NotRightTr -> &NotRightTri + new Transition (5762, 5763), // &NotSucceedsT -> &NotSucceedsTi + new Transition (5781, 5782), // &NotT -> &NotTi + new Transition (5803, 5804), // &NotTildeT -> &NotTildeTi + new Transition (5812, 5813), // &NotVert -> &NotVerti + new Transition (5837, 5838), // &npol -> &npoli + new Transition (5855, 5879), // &nr -> &nri + new Transition (5868, 5869), // &nR -> &nRi + new Transition (5890, 5891), // &nrtr -> &nrtri + new Transition (5895, 5927), // &ns -> &nsi + new Transition (5914, 5915), // &nshortm -> &nshortmi + new Transition (5934, 5935), // &nsm -> &nsmi + new Transition (5988, 5998), // &nt -> &nti + new Transition (5992, 5993), // &Nt -> &Nti + new Transition (6006, 6007), // &ntr -> &ntri + new Transition (6021, 6022), // &ntriangler -> &ntriangleri + new Transition (6043, 6078), // &nv -> &nvi + new Transition (6080, 6081), // &nvinf -> &nvinfi + new Transition (6093, 6094), // &nvltr -> &nvltri + new Transition (6103, 6104), // &nvrtr -> &nvrtri + new Transition (6107, 6108), // &nvs -> &nvsi + new Transition (6138, 6234), // &o -> &oi + new Transition (6148, 6149), // &oc -> &oci + new Transition (6152, 6153), // &Oc -> &Oci + new Transition (6163, 6179), // &od -> &odi + new Transition (6191, 6192), // &OEl -> &OEli + new Transition (6196, 6197), // &oel -> &oeli + new Transition (6201, 6202), // &ofc -> &ofci + new Transition (6238, 6252), // &ol -> &oli + new Transition (6243, 6244), // &olc -> &olci + new Transition (6258, 6276), // &Om -> &Omi + new Transition (6263, 6282), // &om -> &omi + new Transition (6342, 6360), // &or -> &ori + new Transition (6399, 6400), // &Ot -> &Oti + new Transition (6405, 6406), // &ot -> &oti + new Transition (6459, 6460), // &OverParenthes -> &OverParenthesi + new Transition (6463, 6543), // &p -> &pi + new Transition (6474, 6475), // &pars -> &parsi + new Transition (6482, 6541), // &P -> &Pi + new Transition (6485, 6486), // &Part -> &Parti + new Transition (6498, 6503), // &per -> &peri + new Transition (6507, 6508), // &perm -> &permi + new Transition (6524, 6525), // &Ph -> &Phi + new Transition (6527, 6528), // &ph -> &phi + new Transition (6570, 6571), // &plusac -> &plusaci + new Transition (6576, 6577), // &plusc -> &plusci + new Transition (6590, 6591), // &PlusM -> &PlusMi + new Transition (6599, 6600), // &pluss -> &plussi + new Transition (6609, 6610), // &Po -> &Poi + new Transition (6622, 6623), // &po -> &poi + new Transition (6625, 6626), // &point -> &pointi + new Transition (6640, 6725), // &Pr -> &Pri + new Transition (6642, 6729), // &pr -> &pri + new Transition (6696, 6697), // &PrecedesT -> &PrecedesTi + new Transition (6717, 6718), // &precns -> &precnsi + new Transition (6721, 6722), // &precs -> &precsi + new Transition (6741, 6742), // &prns -> &prnsi + new Transition (6760, 6761), // &profl -> &profli + new Transition (6775, 6776), // &Proport -> &Proporti + new Transition (6786, 6787), // &prs -> &prsi + new Transition (6795, 6803), // &Ps -> &Psi + new Transition (6799, 6805), // &ps -> &psi + new Transition (6817, 6821), // &q -> &qi + new Transition (6834, 6835), // &qpr -> &qpri + new Transition (6849, 6858), // &quat -> &quati + new Transition (6852, 6853), // &quatern -> &quaterni + new Transition (6876, 7199), // &r -> &ri + new Transition (6886, 7171), // &R -> &Ri + new Transition (6897, 6898), // &rad -> &radi + new Transition (6956, 6957), // &rarrs -> &rarrsi + new Transition (6969, 6970), // &rAta -> &rAtai + new Transition (6973, 6978), // &rat -> &rati + new Transition (6974, 6975), // &rata -> &ratai + new Transition (7034, 7035), // &Rced -> &Rcedi + new Transition (7038, 7043), // &rce -> &rcei + new Transition (7039, 7040), // &rced -> &rcedi + new Transition (7076, 7078), // &real -> &reali + new Transition (7111, 7112), // &ReverseEqu -> &ReverseEqui + new Transition (7113, 7114), // &ReverseEquil -> &ReverseEquili + new Transition (7116, 7117), // &ReverseEquilibr -> &ReverseEquilibri + new Transition (7125, 7126), // &ReverseUpEqu -> &ReverseUpEqui + new Transition (7127, 7128), // &ReverseUpEquil -> &ReverseUpEquili + new Transition (7130, 7131), // &ReverseUpEquilibr -> &ReverseUpEquilibri + new Transition (7135, 7136), // &rf -> &rfi + new Transition (7224, 7225), // &rightarrowta -> &rightarrowtai + new Transition (7229, 7230), // &RightCe -> &RightCei + new Transition (7231, 7232), // &RightCeil -> &RightCeili + new Transition (7314, 7315), // &rightr -> &rightri + new Transition (7328, 7329), // &rightsqu -> &rightsqui + new Transition (7359, 7360), // &rightthreet -> &rightthreeti + new Transition (7365, 7366), // &RightTr -> &RightTri + new Transition (7431, 7432), // &ris -> &risi + new Transition (7465, 7466), // &rnm -> &rnmi + new Transition (7495, 7496), // &rot -> &roti + new Transition (7507, 7508), // &RoundImpl -> &RoundImpli + new Transition (7521, 7522), // &rppol -> &rppoli + new Transition (7531, 7532), // &Rr -> &Rri + new Transition (7567, 7573), // &rt -> &rti + new Transition (7578, 7579), // &rtr -> &rtri + new Transition (7587, 7588), // &rtriltr -> &rtriltri + new Transition (7610, 7833), // &S -> &Si + new Transition (7617, 7838), // &s -> &si + new Transition (7629, 7662), // &Sc -> &Sci + new Transition (7631, 7666), // &sc -> &sci + new Transition (7654, 7655), // &Sced -> &Scedi + new Transition (7658, 7659), // &sced -> &scedi + new Transition (7676, 7677), // &scns -> &scnsi + new Transition (7682, 7683), // &scpol -> &scpoli + new Transition (7687, 7688), // &scs -> &scsi + new Transition (7721, 7722), // &sem -> &semi + new Transition (7730, 7731), // &setm -> &setmi + new Transition (7799, 7800), // &shortm -> &shortmi + new Transition (7812, 7813), // &ShortR -> &ShortRi + new Transition (7887, 7888), // &SmallC -> &SmallCi + new Transition (7894, 7918), // &sm -> &smi + new Transition (7901, 7902), // &smallsetm -> &smallsetmi + new Transition (7962, 7963), // &spadesu -> &spadesui + new Transition (8027, 8028), // &SquareIntersect -> &SquareIntersecti + new Transition (8059, 8060), // &SquareUn -> &SquareUni + new Transition (8086, 8087), // &ssm -> &ssmi + new Transition (8107, 8108), // &stra -> &strai + new Transition (8114, 8115), // &straighteps -> &straightepsi + new Transition (8121, 8122), // &straightph -> &straightphi + new Transition (8169, 8190), // &subs -> &subsi + new Transition (8240, 8241), // &SucceedsT -> &SucceedsTi + new Transition (8261, 8262), // &succns -> &succnsi + new Transition (8265, 8266), // &succs -> &succsi + new Transition (8352, 8367), // &sups -> &supsi + new Transition (8396, 8397), // &szl -> &szli + new Transition (8400, 8544), // &T -> &Ti + new Transition (8404, 8549), // &t -> &ti + new Transition (8432, 8433), // &Tced -> &Tcedi + new Transition (8437, 8438), // &tced -> &tcedi + new Transition (8461, 8493), // &th -> &thi + new Transition (8467, 8507), // &Th -> &Thi + new Transition (8503, 8504), // &thicks -> &thicksi + new Transition (8531, 8532), // &thks -> &thksi + new Transition (8570, 8571), // &TildeT -> &TildeTi + new Transition (8600, 8601), // &topc -> &topci + new Transition (8618, 8619), // &tpr -> &tpri + new Transition (8628, 8633), // &tr -> &tri + new Transition (8655, 8656), // &triangler -> &triangleri + new Transition (8670, 8671), // &trim -> &trimi + new Transition (8676, 8677), // &Tr -> &Tri + new Transition (8693, 8694), // &trit -> &triti + new Transition (8700, 8701), // &trpez -> &trpezi + new Transition (8737, 8738), // &tw -> &twi + new Transition (8757, 8758), // &twoheadr -> &twoheadri + new Transition (8793, 8794), // &Uarroc -> &Uarroci + new Transition (8815, 8816), // &Uc -> &Uci + new Transition (8820, 8821), // &uc -> &uci + new Transition (8849, 8850), // &uf -> &ufi + new Transition (8901, 8902), // &ultr -> &ultri + new Transition (8916, 8945), // &Un -> &Uni + new Transition (8941, 8942), // &UnderParenthes -> &UnderParenthesi + new Transition (9036, 9037), // &UpEqu -> &UpEqui + new Transition (9038, 9039), // &UpEquil -> &UpEquili + new Transition (9041, 9042), // &UpEquilibr -> &UpEquilibri + new Transition (9058, 9059), // &upharpoonr -> &upharpoonri + new Transition (9081, 9082), // &UpperR -> &UpperRi + new Transition (9092, 9093), // &Ups -> &Upsi + new Transition (9095, 9096), // &ups -> &upsi + new Transition (9127, 9145), // &ur -> &uri + new Transition (9140, 9141), // &Ur -> &Uri + new Transition (9150, 9151), // &urtr -> &urtri + new Transition (9161, 9172), // &ut -> &uti + new Transition (9166, 9167), // &Ut -> &Uti + new Transition (9177, 9178), // &utr -> &utri + new Transition (9211, 9212), // &vareps -> &varepsi + new Transition (9226, 9227), // &varnoth -> &varnothi + new Transition (9231, 9235), // &varp -> &varpi + new Transition (9232, 9233), // &varph -> &varphi + new Transition (9252, 9253), // &vars -> &varsi + new Transition (9285, 9286), // &vartr -> &vartri + new Transition (9297, 9298), // &vartriangler -> &vartriangleri + new Transition (9356, 9357), // &vell -> &velli + new Transition (9370, 9374), // &Vert -> &Verti + new Transition (9382, 9383), // &VerticalL -> &VerticalLi + new Transition (9397, 9398), // &VerticalT -> &VerticalTi + new Transition (9405, 9406), // &VeryTh -> &VeryThi + new Transition (9422, 9423), // &vltr -> &vltri + new Transition (9447, 9448), // &vrtr -> &vrtri + new Transition (9477, 9478), // &vz -> &vzi + new Transition (9485, 9486), // &Wc -> &Wci + new Transition (9491, 9492), // &wc -> &wci + new Transition (9496, 9512), // &we -> &wei + new Transition (9548, 9583), // &x -> &xi + new Transition (9549, 9553), // &xc -> &xci + new Transition (9562, 9563), // &xdtr -> &xdtri + new Transition (9565, 9581), // &X -> &Xi + new Transition (9598, 9599), // &xn -> &xni + new Transition (9618, 9619), // &xot -> &xoti + new Transition (9652, 9653), // &xutr -> &xutri + new Transition (9672, 9712), // &y -> &yi + new Transition (9685, 9686), // &Yc -> &Yci + new Transition (9690, 9691), // &yc -> &yci + new Transition (9754, 9825), // &z -> &zi + new Transition (9794, 9795) // &ZeroW -> &ZeroWi + }; + TransitionTable_j = new Transition[11] { + new Transition (0, 3561), // & -> &j + new Transition (1432, 1665), // &d -> &dj + new Transition (2503, 2587), // &f -> &fj + new Transition (2701, 2820), // &g -> &gj + new Transition (2824, 2830), // &gl -> &glj + new Transition (3243, 3325), // &i -> &ij + new Transition (3624, 3672), // &k -> &kj + new Transition (3692, 4342), // &l -> &lj + new Transition (4965, 5252), // &n -> &nj + new Transition (9848, 9849), // &zw -> &zwj + new Transition (9851, 9852) // &zwn -> &zwnj + }; + TransitionTable_k = new Transition[69] { + new Transition (0, 3624), // & -> &k + new Transition (301, 513), // &b -> &bk + new Transition (303, 304), // &bac -> &back + new Transition (333, 334), // &Bac -> &Back + new Transition (361, 362), // &bbr -> &bbrk + new Transition (366, 367), // &bbrktbr -> &bbrktbrk + new Transition (519, 566), // &bl -> &blk + new Transition (521, 522), // &blac -> &black + new Transition (563, 564), // &blan -> &blank + new Transition (576, 577), // &bloc -> &block + new Transition (965, 966), // &chec -> &check + new Transition (970, 971), // &checkmar -> &checkmark + new Transition (1070, 1071), // &Cloc -> &Clock + new Transition (1234, 1235), // &CounterCloc -> &CounterClock + new Transition (1463, 1464), // &db -> &dbk + new Transition (2024, 2025), // &drb -> &drbk + new Transition (2059, 2060), // &Dstro -> &Dstrok + new Transition (2064, 2065), // &dstro -> &dstrok + new Transition (2621, 2626), // &for -> &fork + new Transition (3017, 3018), // &Hace -> &Hacek + new Transition (3020, 3112), // &h -> &hk + new Transition (3136, 3137), // &hoo -> &hook + new Transition (3199, 3200), // &Hstro -> &Hstrok + new Transition (3204, 3205), // &hstro -> &hstrok + new Transition (3436, 3437), // &intlarh -> &intlarhk + new Transition (3539, 3540), // &Iu -> &Iuk + new Transition (3544, 3545), // &iu -> &iuk + new Transition (3608, 3609), // &Ju -> &Juk + new Transition (3613, 3614), // &ju -> &juk + new Transition (3776, 3777), // &larrh -> &larrhk + new Transition (3818, 3819), // &lbbr -> &lbbrk + new Transition (3821, 3828), // &lbr -> &lbrk + new Transition (3823, 3826), // &lbrac -> &lbrack + new Transition (3909, 3910), // &LeftAngleBrac -> &LeftAngleBrack + new Transition (3970, 3971), // &LeftDoubleBrac -> &LeftDoubleBrack + new Transition (4335, 4336), // &lhbl -> &lhblk + new Transition (4431, 4432), // &lobr -> &lobrk + new Transition (4686, 4687), // &Lstro -> &Lstrok + new Transition (4691, 4692), // &lstro -> &lstrok + new Transition (4804, 4805), // &mar -> &mark + new Transition (5068, 5069), // &nearh -> &nearhk + new Transition (5106, 5107), // &NegativeThic -> &NegativeThick + new Transition (5351, 5352), // &NoBrea -> &NoBreak + new Transition (5358, 5359), // &NonBrea -> &NonBreak + new Transition (6114, 6115), // &nwarh -> &nwarhk + new Transition (6444, 6447), // &OverBrac -> &OverBrack + new Transition (6515, 6516), // &perten -> &pertenk + new Transition (6550, 6551), // &pitchfor -> &pitchfork + new Transition (6557, 6563), // &plan -> &plank + new Transition (6558, 6559), // &planc -> &planck + new Transition (6947, 6948), // &rarrh -> &rarrhk + new Transition (7002, 7003), // &rbbr -> &rbbrk + new Transition (7005, 7012), // &rbr -> &rbrk + new Transition (7007, 7010), // &rbrac -> &rbrack + new Transition (7183, 7184), // &RightAngleBrac -> &RightAngleBrack + new Transition (7245, 7246), // &RightDoubleBrac -> &RightDoubleBrack + new Transition (7478, 7479), // &robr -> &robrk + new Transition (7706, 7707), // &searh -> &searhk + new Transition (8378, 8379), // &swarh -> &swarhk + new Transition (8416, 8417), // &tbr -> &tbrk + new Transition (8461, 8527), // &th -> &thk + new Transition (8494, 8495), // &thic -> &thick + new Transition (8508, 8509), // &Thic -> &Thick + new Transition (8611, 8612), // &topfor -> &topfork + new Transition (8729, 8730), // &Tstro -> &Tstrok + new Transition (8734, 8735), // &tstro -> &tstrok + new Transition (8884, 8885), // &uhbl -> &uhblk + new Transition (8926, 8929), // &UnderBrac -> &UnderBrack + new Transition (9208, 9217) // &var -> &vark + }; + TransitionTable_l = new Transition[438] { + new Transition (0, 3692), // & -> &l + new Transition (1, 89), // &A -> &Al + new Transition (8, 79), // &a -> &al + new Transition (50, 51), // &AE -> &AEl + new Transition (55, 56), // &ae -> &ael + new Transition (104, 108), // &ama -> &amal + new Transition (128, 129), // &ands -> &andsl + new Transition (136, 140), // &ang -> &angl + new Transition (217, 218), // &App -> &Appl + new Transition (270, 271), // &Ati -> &Atil + new Transition (276, 277), // &ati -> &atil + new Transition (282, 283), // &Aum -> Ä + new Transition (286, 287), // &aum -> ä + new Transition (301, 519), // &b -> &bl + new Transition (313, 314), // &backepsi -> &backepsil + new Transition (335, 336), // &Backs -> &Backsl + new Transition (417, 418), // &Bernou -> &Bernoul + new Transition (418, 419), // &Bernoul -> &Bernoull + new Transition (460, 461), // &bigop -> &bigopl + new Transition (486, 487), // &bigtriang -> &bigtriangl + new Transition (498, 499), // &bigup -> &bigupl + new Transition (522, 523), // &black -> &blackl + new Transition (543, 544), // &blacktriang -> &blacktriangl + new Transition (545, 552), // &blacktriangle -> &blacktrianglel + new Transition (618, 621), // &boxD -> &boxDl + new Transition (623, 626), // &boxd -> &boxdl + new Transition (662, 663), // &boxp -> &boxpl + new Transition (673, 676), // &boxU -> &boxUl + new Transition (678, 681), // &boxu -> &boxul + new Transition (691, 705), // &boxV -> &boxVl + new Transition (693, 709), // &boxv -> &boxvl + new Transition (757, 758), // &bso -> &bsol + new Transition (767, 768), // &bu -> &bul + new Transition (768, 769), // &bul -> &bull + new Transition (789, 1068), // &C -> &Cl + new Transition (796, 1117), // &c -> &cl + new Transition (830, 831), // &Capita -> &Capital + new Transition (842, 843), // &CapitalDifferentia -> &CapitalDifferential + new Transition (855, 856), // &Cay -> &Cayl + new Transition (878, 879), // &Ccedi -> Ç + new Transition (883, 884), // &ccedi -> ç + new Transition (917, 918), // &cedi -> ¸ + new Transition (922, 923), // &Cedi -> &Cedil + new Transition (923, 924), // &Cedil -> &Cedill + new Transition (981, 986), // &circ -> &circl + new Transition (992, 993), // &circlearrow -> &circlearrowl + new Transition (1021, 1022), // &Circ -> &Circl + new Transition (1038, 1039), // &CircleP -> &CirclePl + new Transition (1089, 1090), // &ClockwiseContourIntegra -> &ClockwiseContourIntegral + new Transition (1096, 1097), // &CloseCur -> &CloseCurl + new Transition (1102, 1103), // &CloseCurlyDoub -> &CloseCurlyDoubl + new Transition (1126, 1127), // &Co -> &Col + new Transition (1131, 1132), // &co -> &col + new Transition (1148, 1153), // &comp -> &compl + new Transition (1197, 1198), // &ContourIntegra -> &ContourIntegral + new Transition (1231, 1232), // &CounterC -> &CounterCl + new Transition (1253, 1254), // &CounterClockwiseContourIntegra -> &CounterClockwiseContourIntegral + new Transition (1292, 1308), // &cu -> &cul + new Transition (1296, 1297), // &cudarr -> &cudarrl + new Transition (1346, 1353), // &cur -> &curl + new Transition (1387, 1388), // &curvearrow -> &curvearrowl + new Transition (1419, 1420), // &cy -> &cyl + new Transition (1432, 1669), // &d -> &dl + new Transition (1433, 1439), // &da -> &dal + new Transition (1463, 1470), // &db -> &dbl + new Transition (1516, 1525), // &de -> &del + new Transition (1519, 1520), // &De -> &Del + new Transition (1552, 1553), // &dhar -> &dharl + new Transition (1565, 1566), // &Diacritica -> &Diacritical + new Transition (1578, 1579), // &DiacriticalDoub -> &DiacriticalDoubl + new Transition (1594, 1595), // &DiacriticalTi -> &DiacriticalTil + new Transition (1629, 1630), // &Differentia -> &Differential + new Transition (1679, 1680), // &do -> &dol + new Transition (1680, 1681), // &dol -> &doll + new Transition (1710, 1711), // &DotEqua -> &DotEqual + new Transition (1719, 1720), // &dotp -> &dotpl + new Transition (1732, 1733), // &doub -> &doubl + new Transition (1745, 1746), // &Doub -> &Doubl + new Transition (1761, 1762), // &DoubleContourIntegra -> &DoubleContourIntegral + new Transition (1875, 1876), // &DoubleVertica -> &DoubleVertical + new Transition (1938, 1939), // &downharpoon -> &downharpoonl + new Transition (2054, 2055), // &dso -> &dsol + new Transition (2089, 2090), // &dwang -> &dwangl + new Transition (2108, 2206), // &E -> &El + new Transition (2115, 2204), // &e -> &el + new Transition (2148, 2149), // &eco -> &ecol + new Transition (2204, 2220), // &el -> &ell + new Transition (2251, 2252), // &EmptySma -> &EmptySmal + new Transition (2252, 2253), // &EmptySmal -> &EmptySmall + new Transition (2269, 2270), // &EmptyVerySma -> &EmptyVerySmal + new Transition (2270, 2271), // &EmptyVerySmal -> &EmptyVerySmall + new Transition (2312, 2319), // &ep -> &epl + new Transition (2316, 2317), // &epars -> &eparsl + new Transition (2324, 2333), // &epsi -> &epsil + new Transition (2328, 2329), // &Epsi -> &Epsil + new Transition (2345, 2346), // &eqco -> &eqcol + new Transition (2350, 2354), // &eqs -> &eqsl + new Transition (2357, 2362), // &eqslant -> &eqslantl + new Transition (2369, 2370), // &Equa -> &Equal + new Transition (2373, 2374), // &equa -> &equal + new Transition (2378, 2379), // &EqualTi -> &EqualTil + new Transition (2387, 2388), // &Equi -> &Equil + new Transition (2406, 2407), // &eqvpars -> &eqvparsl + new Transition (2448, 2449), // &Eum -> Ë + new Transition (2452, 2453), // &eum -> ë + new Transition (2459, 2460), // &exc -> &excl + new Transition (2489, 2490), // &Exponentia -> &Exponential + new Transition (2499, 2500), // &exponentia -> &exponential + new Transition (2503, 2592), // &f -> &fl + new Transition (2504, 2505), // &fa -> &fal + new Transition (2505, 2506), // &fal -> &fall + new Transition (2526, 2527), // &fema -> &femal + new Transition (2530, 2536), // &ff -> &ffl + new Transition (2531, 2532), // &ffi -> &ffil + new Transition (2536, 2540), // &ffl -> &ffll + new Transition (2549, 2550), // &fi -> &fil + new Transition (2554, 2555), // &Fi -> &Fil + new Transition (2555, 2556), // &Fil -> &Fill + new Transition (2561, 2562), // &FilledSma -> &FilledSmal + new Transition (2562, 2563), // &FilledSmal -> &FilledSmall + new Transition (2577, 2578), // &FilledVerySma -> &FilledVerySmal + new Transition (2578, 2579), // &FilledVerySmal -> &FilledVerySmall + new Transition (2587, 2588), // &fj -> &fjl + new Transition (2592, 2596), // &fl -> &fll + new Transition (2617, 2618), // &ForA -> &ForAl + new Transition (2618, 2619), // &ForAl -> &ForAll + new Transition (2622, 2623), // &fora -> &foral + new Transition (2623, 2624), // &foral -> &forall + new Transition (2686, 2687), // &fras -> &frasl + new Transition (2701, 2824), // &g -> &gl + new Transition (2739, 2740), // &Gcedi -> &Gcedil + new Transition (2763, 2767), // &gE -> &gEl + new Transition (2765, 2769), // &ge -> &gel + new Transition (2775, 2776), // &geqs -> &geqsl + new Transition (2781, 2794), // &ges -> &gesl + new Transition (2790, 2792), // &gesdoto -> &gesdotol + new Transition (2813, 2814), // &gime -> &gimel + new Transition (2875, 2876), // &GreaterEqua -> &GreaterEqual + new Transition (2884, 2885), // &GreaterFu -> &GreaterFul + new Transition (2885, 2886), // &GreaterFul -> &GreaterFull + new Transition (2890, 2891), // &GreaterFullEqua -> &GreaterFullEqual + new Transition (2906, 2907), // &GreaterS -> &GreaterSl + new Transition (2914, 2915), // &GreaterSlantEqua -> &GreaterSlantEqual + new Transition (2918, 2919), // &GreaterTi -> &GreaterTil + new Transition (2932, 2936), // &gsim -> &gsiml + new Transition (2942, 2954), // > -> >l + new Transition (2965, 2993), // >r -> >rl + new Transition (2981, 2982), // >req -> >reql + new Transition (2987, 2988), // >reqq -> >reqql + new Transition (3021, 3027), // &ha -> &hal + new Transition (3031, 3032), // &hami -> &hamil + new Transition (3074, 3084), // &he -> &hel + new Transition (3084, 3085), // &hel -> &hell + new Transition (3100, 3101), // &Hi -> &Hil + new Transition (3137, 3138), // &hook -> &hookl + new Transition (3177, 3178), // &Horizonta -> &Horizontal + new Transition (3188, 3192), // &hs -> &hsl + new Transition (3222, 3223), // &HumpEqua -> &HumpEqual + new Transition (3227, 3228), // &hybu -> &hybul + new Transition (3228, 3229), // &hybul -> &hybull + new Transition (3278, 3279), // &iexc -> ¡ + new Transition (3320, 3321), // &IJ -> &IJl + new Transition (3325, 3326), // &ij -> &ijl + new Transition (3341, 3352), // &imag -> &imagl + new Transition (3372, 3373), // &Imp -> &Impl + new Transition (3401, 3433), // &int -> &intl + new Transition (3404, 3405), // &intca -> &intcal + new Transition (3416, 3417), // &Integra -> &Integral + new Transition (3421, 3422), // &interca -> &intercal + new Transition (3448, 3449), // &Invisib -> &Invisibl + new Transition (3529, 3530), // &Iti -> &Itil + new Transition (3534, 3535), // &iti -> &itil + new Transition (3549, 3550), // &Ium -> Ï + new Transition (3552, 3553), // &ium -> ï + new Transition (3635, 3636), // &Kcedi -> &Kcedil + new Transition (3641, 3642), // &kcedi -> &kcedil + new Transition (3692, 4348), // &l -> &ll + new Transition (3698, 4346), // &L -> &Ll + new Transition (3737, 3741), // &lang -> &langl + new Transition (3746, 3747), // &Lap -> &Lapl + new Transition (3766, 3779), // &larr -> &larrl + new Transition (3782, 3783), // &larrp -> &larrpl + new Transition (3789, 3790), // &larrt -> &larrtl + new Transition (3796, 3797), // &lAtai -> &lAtail + new Transition (3800, 3801), // &latai -> &latail + new Transition (3831, 3832), // &lbrks -> &lbrksl + new Transition (3851, 3852), // &Lcedi -> &Lcedil + new Transition (3856, 3857), // &lcedi -> &lcedil + new Transition (3859, 3860), // &lcei -> &lceil + new Transition (3903, 3904), // &LeftAng -> &LeftAngl + new Transition (3926, 4019), // &left -> &leftl + new Transition (3950, 3951), // &leftarrowtai -> &leftarrowtail + new Transition (3955, 3956), // &LeftCei -> &LeftCeil + new Transition (3964, 3965), // &LeftDoub -> &LeftDoubl + new Transition (3998, 3999), // &LeftF -> &LeftFl + new Transition (4124, 4125), // &LeftTriang -> &LeftTriangl + new Transition (4135, 4136), // &LeftTriangleEqua -> &LeftTriangleEqual + new Transition (4191, 4192), // &leqs -> &leqsl + new Transition (4243, 4244), // &LessEqua -> &LessEqual + new Transition (4254, 4255), // &LessFu -> &LessFul + new Transition (4255, 4256), // &LessFul -> &LessFull + new Transition (4260, 4261), // &LessFullEqua -> &LessFullEqual + new Transition (4284, 4285), // &LessS -> &LessSl + new Transition (4292, 4293), // &LessSlantEqua -> &LessSlantEqual + new Transition (4296, 4297), // &LessTi -> &LessTil + new Transition (4301, 4307), // &lf -> &lfl + new Transition (4330, 4332), // &lharu -> &lharul + new Transition (4334, 4335), // &lhb -> &lhbl + new Transition (4436, 4447), // &Long -> &Longl + new Transition (4458, 4459), // &long -> &longl + new Transition (4548, 4549), // &looparrow -> &looparrowl + new Transition (4560, 4569), // &lop -> &lopl + new Transition (4623, 4625), // &lpar -> &lparl + new Transition (4698, 4720), // < -> <l + new Transition (4767, 4909), // &m -> &ml + new Transition (4768, 4772), // &ma -> &mal + new Transition (4789, 4796), // &mapsto -> &mapstol + new Transition (4839, 4840), // &measuredang -> &measuredangl + new Transition (4843, 4854), // &Me -> &Mel + new Transition (4854, 4855), // &Mel -> &Mell + new Transition (4904, 4905), // &MinusP -> &MinusPl + new Transition (4917, 4918), // &mnp -> &mnpl + new Transition (4924, 4925), // &mode -> &model + new Transition (4952, 4954), // &mu -> &mul + new Transition (4965, 5256), // &n -> &nl + new Transition (4967, 4968), // &nab -> &nabl + new Transition (5005, 5006), // &natura -> &natural + new Transition (5036, 5037), // &Ncedi -> &Ncedil + new Transition (5041, 5042), // &ncedi -> &ncedil + new Transition (5204, 5205), // &ngeqs -> &ngeqsl + new Transition (5272, 5326), // &nL -> &nLl + new Transition (5316, 5317), // &nleqs -> &nleqsl + new Transition (5399, 5400), // &NotDoub -> &NotDoubl + new Transition (5408, 5409), // &NotDoubleVertica -> &NotDoubleVertical + new Transition (5414, 5415), // &NotE -> &NotEl + new Transition (5424, 5425), // &NotEqua -> &NotEqual + new Transition (5428, 5429), // &NotEqualTi -> &NotEqualTil + new Transition (5450, 5451), // &NotGreaterEqua -> &NotGreaterEqual + new Transition (5454, 5455), // &NotGreaterFu -> &NotGreaterFul + new Transition (5455, 5456), // &NotGreaterFul -> &NotGreaterFull + new Transition (5460, 5461), // &NotGreaterFullEqua -> &NotGreaterFullEqual + new Transition (5476, 5477), // &NotGreaterS -> &NotGreaterSl + new Transition (5484, 5485), // &NotGreaterSlantEqua -> &NotGreaterSlantEqual + new Transition (5488, 5489), // &NotGreaterTi -> &NotGreaterTil + new Transition (5509, 5510), // &NotHumpEqua -> &NotHumpEqual + new Transition (5537, 5538), // &NotLeftTriang -> &NotLeftTriangl + new Transition (5548, 5549), // &NotLeftTriangleEqua -> &NotLeftTriangleEqual + new Transition (5557, 5558), // &NotLessEqua -> &NotLessEqual + new Transition (5573, 5574), // &NotLessS -> &NotLessSl + new Transition (5581, 5582), // &NotLessSlantEqua -> &NotLessSlantEqual + new Transition (5585, 5586), // &NotLessTi -> &NotLessTil + new Transition (5642, 5643), // &NotPrecedesEqua -> &NotPrecedesEqual + new Transition (5645, 5646), // &NotPrecedesS -> &NotPrecedesSl + new Transition (5653, 5654), // &NotPrecedesSlantEqua -> &NotPrecedesSlantEqual + new Transition (5663, 5664), // &NotReverseE -> &NotReverseEl + new Transition (5680, 5681), // &NotRightTriang -> &NotRightTriangl + new Transition (5691, 5692), // &NotRightTriangleEqua -> &NotRightTriangleEqual + new Transition (5710, 5711), // &NotSquareSubsetEqua -> &NotSquareSubsetEqual + new Transition (5723, 5724), // &NotSquareSupersetEqua -> &NotSquareSupersetEqual + new Transition (5735, 5736), // &NotSubsetEqua -> &NotSubsetEqual + new Transition (5748, 5749), // &NotSucceedsEqua -> &NotSucceedsEqual + new Transition (5751, 5752), // &NotSucceedsS -> &NotSucceedsSl + new Transition (5759, 5760), // &NotSucceedsSlantEqua -> &NotSucceedsSlantEqual + new Transition (5763, 5764), // &NotSucceedsTi -> &NotSucceedsTil + new Transition (5778, 5779), // &NotSupersetEqua -> &NotSupersetEqual + new Transition (5782, 5783), // &NotTi -> &NotTil + new Transition (5790, 5791), // &NotTildeEqua -> &NotTildeEqual + new Transition (5794, 5795), // &NotTildeFu -> &NotTildeFul + new Transition (5795, 5796), // &NotTildeFul -> &NotTildeFull + new Transition (5800, 5801), // &NotTildeFullEqua -> &NotTildeFullEqual + new Transition (5804, 5805), // &NotTildeTi -> &NotTildeTil + new Transition (5815, 5816), // &NotVertica -> &NotVertical + new Transition (5825, 5826), // &npara -> &nparal + new Transition (5826, 5827), // &nparal -> &nparall + new Transition (5828, 5829), // &nparalle -> &nparallel + new Transition (5831, 5832), // &npars -> &nparsl + new Transition (5836, 5837), // &npo -> &npol + new Transition (5921, 5922), // &nshortpara -> &nshortparal + new Transition (5922, 5923), // &nshortparal -> &nshortparall + new Transition (5924, 5925), // &nshortparalle -> &nshortparallel + new Transition (5988, 6003), // &nt -> &ntl + new Transition (5989, 5990), // &ntg -> &ntgl + new Transition (5993, 5994), // &Nti -> &Ntil + new Transition (5998, 5999), // &nti -> &ntil + new Transition (6010, 6011), // &ntriang -> &ntriangl + new Transition (6012, 6013), // &ntriangle -> &ntrianglel + new Transition (6043, 6084), // &nv -> &nvl + new Transition (6138, 6238), // &o -> &ol + new Transition (6169, 6170), // &Odb -> &Odbl + new Transition (6174, 6175), // &odb -> &odbl + new Transition (6186, 6187), // &odso -> &odsol + new Transition (6190, 6191), // &OE -> &OEl + new Transition (6195, 6196), // &oe -> &oel + new Transition (6302, 6336), // &op -> &opl + new Transition (6311, 6312), // &OpenCur -> &OpenCurl + new Transition (6317, 6318), // &OpenCurlyDoub -> &OpenCurlyDoubl + new Transition (6368, 6369), // &ors -> &orsl + new Transition (6378, 6386), // &Os -> &Osl + new Transition (6382, 6391), // &os -> &osl + new Transition (6396, 6397), // &oso -> &osol + new Transition (6400, 6401), // &Oti -> &Otil + new Transition (6406, 6407), // &oti -> &otil + new Transition (6423, 6424), // &Oum -> Ö + new Transition (6427, 6428), // &oum -> ö + new Transition (6463, 6555), // &p -> &pl + new Transition (6467, 6469), // ¶ -> ¶l + new Transition (6469, 6470), // ¶l -> ¶ll + new Transition (6471, 6472), // ¶lle -> ¶llel + new Transition (6474, 6478), // &pars -> &parsl + new Transition (6482, 6587), // &P -> &Pl + new Transition (6487, 6488), // &Partia -> &Partial + new Transition (6508, 6509), // &permi -> &permil + new Transition (6616, 6617), // &Poincarep -> &Poincarepl + new Transition (6666, 6667), // &preccur -> &preccurl + new Transition (6682, 6683), // &PrecedesEqua -> &PrecedesEqual + new Transition (6685, 6686), // &PrecedesS -> &PrecedesSl + new Transition (6693, 6694), // &PrecedesSlantEqua -> &PrecedesSlantEqual + new Transition (6697, 6698), // &PrecedesTi -> &PrecedesTil + new Transition (6754, 6760), // &prof -> &profl + new Transition (6755, 6756), // &profa -> &profal + new Transition (6780, 6781), // &Proportiona -> &Proportional + new Transition (6792, 6793), // &prure -> &prurel + new Transition (6876, 7442), // &r -> &rl + new Transition (6912, 6918), // &rang -> &rangl + new Transition (6932, 6950), // &rarr -> &rarrl + new Transition (6953, 6954), // &rarrp -> &rarrpl + new Transition (6960, 6961), // &Rarrt -> &Rarrtl + new Transition (6963, 6964), // &rarrt -> &rarrtl + new Transition (6970, 6971), // &rAtai -> &rAtail + new Transition (6975, 6976), // &ratai -> &ratail + new Transition (6982, 6983), // &rationa -> &rational + new Transition (7015, 7016), // &rbrks -> &rbrksl + new Transition (7035, 7036), // &Rcedi -> &Rcedil + new Transition (7040, 7041), // &rcedi -> &rcedil + new Transition (7043, 7044), // &rcei -> &rceil + new Transition (7053, 7057), // &rd -> &rdl + new Transition (7075, 7076), // &rea -> &real + new Transition (7102, 7103), // &ReverseE -> &ReverseEl + new Transition (7112, 7113), // &ReverseEqui -> &ReverseEquil + new Transition (7126, 7127), // &ReverseUpEqui -> &ReverseUpEquil + new Transition (7135, 7141), // &rf -> &rfl + new Transition (7160, 7162), // &rharu -> &rharul + new Transition (7177, 7178), // &RightAng -> &RightAngl + new Transition (7202, 7294), // &right -> &rightl + new Transition (7225, 7226), // &rightarrowtai -> &rightarrowtail + new Transition (7230, 7231), // &RightCei -> &RightCeil + new Transition (7239, 7240), // &RightDoub -> &RightDoubl + new Transition (7273, 7274), // &RightF -> &RightFl + new Transition (7369, 7370), // &RightTriang -> &RightTriangl + new Transition (7380, 7381), // &RightTriangleEqua -> &RightTriangleEqual + new Transition (7481, 7491), // &rop -> &ropl + new Transition (7506, 7507), // &RoundImp -> &RoundImpl + new Transition (7520, 7521), // &rppo -> &rppol + new Transition (7579, 7585), // &rtri -> &rtril + new Transition (7590, 7591), // &Ru -> &Rul + new Transition (7594, 7595), // &RuleDe -> &RuleDel + new Transition (7601, 7602), // &ru -> &rul + new Transition (7617, 7878), // &s -> &sl + new Transition (7655, 7656), // &Scedi -> &Scedil + new Transition (7659, 7660), // &scedi -> &scedil + new Transition (7681, 7682), // &scpo -> &scpol + new Transition (7806, 7807), // &shortpara -> &shortparal + new Transition (7807, 7808), // &shortparal -> &shortparall + new Transition (7809, 7810), // &shortparalle -> &shortparallel + new Transition (7847, 7861), // &sim -> &siml + new Transition (7868, 7869), // &simp -> &simpl + new Transition (7884, 7885), // &Sma -> &Smal + new Transition (7885, 7886), // &Smal -> &Small + new Transition (7890, 7891), // &SmallCirc -> &SmallCircl + new Transition (7895, 7896), // &sma -> &smal + new Transition (7896, 7897), // &smal -> &small + new Transition (7915, 7916), // &smepars -> &smeparsl + new Transition (7918, 7921), // &smi -> &smil + new Transition (7936, 7942), // &so -> &sol + new Transition (8042, 8043), // &SquareSubsetEqua -> &SquareSubsetEqual + new Transition (8055, 8056), // &SquareSupersetEqua -> &SquareSupersetEqual + new Transition (8087, 8088), // &ssmi -> &ssmil + new Transition (8115, 8116), // &straightepsi -> &straightepsil + new Transition (8146, 8147), // &submu -> &submul + new Transition (8155, 8156), // &subp -> &subpl + new Transition (8181, 8182), // &SubsetEqua -> &SubsetEqual + new Transition (8210, 8211), // &succcur -> &succcurl + new Transition (8226, 8227), // &SucceedsEqua -> &SucceedsEqual + new Transition (8229, 8230), // &SucceedsS -> &SucceedsSl + new Transition (8237, 8238), // &SucceedsSlantEqua -> &SucceedsSlantEqual + new Transition (8241, 8242), // &SucceedsTi -> &SucceedsTil + new Transition (8284, 8328), // &sup -> &supl + new Transition (8317, 8318), // &SupersetEqua -> &SupersetEqual + new Transition (8322, 8323), // &suphso -> &suphsol + new Transition (8334, 8335), // &supmu -> &supmul + new Transition (8343, 8344), // &supp -> &suppl + new Transition (8395, 8396), // &sz -> &szl + new Transition (8433, 8434), // &Tcedi -> &Tcedil + new Transition (8438, 8439), // &tcedi -> &tcedil + new Transition (8449, 8450), // &te -> &tel + new Transition (8544, 8545), // &Ti -> &Til + new Transition (8549, 8550), // &ti -> &til + new Transition (8557, 8558), // &TildeEqua -> &TildeEqual + new Transition (8561, 8562), // &TildeFu -> &TildeFul + new Transition (8562, 8563), // &TildeFul -> &TildeFull + new Transition (8567, 8568), // &TildeFullEqua -> &TildeFullEqual + new Transition (8571, 8572), // &TildeTi -> &TildeTil + new Transition (8636, 8637), // &triang -> &triangl + new Transition (8638, 8645), // &triangle -> &trianglel + new Transition (8678, 8679), // &Trip -> &Tripl + new Transition (8685, 8686), // &trip -> &tripl + new Transition (8746, 8747), // &twohead -> &twoheadl + new Transition (8775, 8887), // &u -> &ul + new Transition (8835, 8836), // &Udb -> &Udbl + new Transition (8840, 8841), // &udb -> &udbl + new Transition (8878, 8879), // &uhar -> &uharl + new Transition (8883, 8884), // &uhb -> &uhbl + new Transition (8909, 8914), // &um -> ¨ + new Transition (8949, 8950), // &UnionP -> &UnionPl + new Transition (8983, 9064), // &up -> &upl + new Transition (9037, 9038), // &UpEqui -> &UpEquil + new Transition (9052, 9053), // &upharpoon -> &upharpoonl + new Transition (9093, 9100), // &Upsi -> &Upsil + new Transition (9096, 9104), // &upsi -> &upsil + new Transition (9167, 9168), // &Uti -> &Util + new Transition (9172, 9173), // &uti -> &util + new Transition (9188, 9189), // &Uum -> Ü + new Transition (9191, 9192), // &uum -> ü + new Transition (9197, 9198), // &uwang -> &uwangl + new Transition (9201, 9420), // &v -> &vl + new Transition (9212, 9213), // &varepsi -> &varepsil + new Transition (9289, 9290), // &vartriang -> &vartriangl + new Transition (9291, 9292), // &vartriangle -> &vartrianglel + new Transition (9328, 9340), // &Vdash -> &Vdashl + new Transition (9345, 9355), // &ve -> &vel + new Transition (9355, 9356), // &vel -> &vell + new Transition (9376, 9377), // &Vertica -> &Vertical + new Transition (9398, 9399), // &VerticalTi -> &VerticalTil + new Transition (9548, 9585), // &x -> &xl + new Transition (9611, 9614), // &xop -> &xopl + new Transition (9646, 9647), // &xup -> &xupl + new Transition (9741, 9742), // &Yum -> &Yuml + new Transition (9744, 9745) // &yum -> ÿ + }; + TransitionTable_m = new Transition[177] { + new Transition (0, 4767), // & -> &m + new Transition (1, 98), // &A -> &Am + new Transition (8, 103), // &a -> &am + new Transition (83, 84), // &alefsy -> &alefsym + new Transition (136, 143), // &ang -> &angm + new Transition (262, 263), // &asy -> &asym + new Transition (281, 282), // &Au -> &Aum + new Transition (285, 286), // &au -> &aum + new Transition (320, 321), // &backpri -> &backprim + new Transition (325, 326), // &backsi -> &backsim + new Transition (384, 399), // &be -> &bem + new Transition (466, 467), // &bigoti -> &bigotim + new Transition (605, 606), // &botto -> &bottom + new Transition (613, 656), // &box -> &boxm + new Transition (668, 669), // &boxti -> &boxtim + new Transition (721, 722), // &bpri -> &bprim + new Transition (748, 749), // &bse -> &bsem + new Transition (752, 753), // &bsi -> &bsim + new Transition (767, 774), // &bu -> &bum + new Transition (781, 782), // &Bu -> &Bum + new Transition (904, 905), // &ccupss -> &ccupssm + new Transition (915, 927), // &ce -> &cem + new Transition (966, 968), // &check -> &checkm + new Transition (979, 1059), // &cir -> &cirm + new Transition (1044, 1045), // &CircleTi -> &CircleTim + new Transition (1131, 1142), // &co -> &com + new Transition (1142, 1143), // &com -> &comm + new Transition (1154, 1155), // &comple -> &complem + new Transition (1349, 1351), // &curarr -> &curarrm + new Transition (1516, 1529), // &de -> &dem + new Transition (1558, 1603), // &Dia -> &Diam + new Transition (1600, 1601), // &dia -> &diam + new Transition (1634, 1635), // &diga -> &digam + new Transition (1635, 1636), // &digam -> &digamm + new Transition (1652, 1653), // ÷onti -> ÷ontim + new Transition (1694, 1713), // &dot -> &dotm + new Transition (2108, 2228), // &E -> &Em + new Transition (2115, 2233), // &e -> &em + new Transition (2207, 2208), // &Ele -> &Elem + new Transition (2249, 2250), // &EmptyS -> &EmptySm + new Transition (2267, 2268), // &EmptyVeryS -> &EmptyVerySm + new Transition (2351, 2352), // &eqsi -> &eqsim + new Transition (2393, 2394), // &Equilibriu -> &Equilibrium + new Transition (2430, 2431), // &Esi -> &Esim + new Transition (2433, 2434), // &esi -> &esim + new Transition (2447, 2448), // &Eu -> &Eum + new Transition (2451, 2452), // &eu -> &eum + new Transition (2524, 2525), // &fe -> &fem + new Transition (2559, 2560), // &FilledS -> &FilledSm + new Transition (2575, 2576), // &FilledVeryS -> &FilledVerySm + new Transition (2702, 2714), // &ga -> &gam + new Transition (2709, 2710), // &Ga -> &Gam + new Transition (2710, 2711), // &Gam -> &Gamm + new Transition (2714, 2715), // &gam -> &gamm + new Transition (2811, 2812), // &gi -> &gim + new Transition (2850, 2851), // &gnsi -> &gnsim + new Transition (2931, 2932), // &gsi -> &gsim + new Transition (2999, 3000), // >rsi -> >rsim + new Transition (3021, 3030), // &ha -> &ham + new Transition (3126, 3131), // &ho -> &hom + new Transition (3207, 3208), // &Hu -> &Hum + new Transition (3215, 3216), // &HumpDownHu -> &HumpDownHum + new Transition (3236, 3330), // &I -> &Im + new Transition (3243, 3336), // &i -> &im + new Transition (3452, 3453), // &InvisibleCo -> &InvisibleCom + new Transition (3453, 3454), // &InvisibleCom -> &InvisibleComm + new Transition (3458, 3459), // &InvisibleTi -> &InvisibleTim + new Transition (3539, 3549), // &Iu -> &Ium + new Transition (3544, 3552), // &iu -> &ium + new Transition (3561, 3577), // &j -> &jm + new Transition (3692, 4385), // &l -> &lm + new Transition (3698, 4379), // &L -> &Lm + new Transition (3699, 3723), // &La -> &Lam + new Transition (3705, 3728), // &la -> &lam + new Transition (3711, 3712), // &lae -> &laem + new Transition (3786, 3787), // &larrsi -> &larrsim + new Transition (4115, 4116), // &leftthreeti -> &leftthreetim + new Transition (4281, 4282), // &lesssi -> &lesssim + new Transition (4419, 4420), // &lnsi -> &lnsim + new Transition (4458, 4502), // &long -> &longm + new Transition (4574, 4575), // &loti -> &lotim + new Transition (4628, 4646), // &lr -> &lrm + new Transition (4669, 4670), // &lsi -> &lsim + new Transition (4715, 4716), // <i -> <im + new Transition (4810, 4811), // &mco -> &mcom + new Transition (4811, 4812), // &mcom -> &mcomm + new Transition (4846, 4847), // &Mediu -> &Medium + new Transition (4952, 4961), // &mu -> &mum + new Transition (4956, 4957), // &multi -> &multim + new Transition (4965, 5343), // &n -> &nm + new Transition (5014, 5015), // &nbu -> &nbum + new Transition (5095, 5096), // &NegativeMediu -> &NegativeMedium + new Transition (5145, 5146), // &nesi -> &nesim + new Transition (5216, 5217), // &ngsi -> &ngsim + new Transition (5329, 5330), // &nlsi -> &nlsim + new Transition (5416, 5417), // &NotEle -> &NotElem + new Transition (5494, 5495), // &NotHu -> &NotHum + new Transition (5502, 5503), // &NotHumpDownHu -> &NotHumpDownHum + new Transition (5665, 5666), // &NotReverseEle -> &NotReverseElem + new Transition (5895, 5934), // &ns -> &nsm + new Transition (5913, 5914), // &nshort -> &nshortm + new Transition (5927, 5928), // &nsi -> &nsim + new Transition (6032, 6034), // &nu -> &num + new Transition (6108, 6109), // &nvsi -> &nvsim + new Transition (6131, 6258), // &O -> &Om + new Transition (6138, 6263), // &o -> &om + new Transition (6227, 6232), // &oh -> &ohm + new Transition (6348, 6358), // &ord -> º + new Transition (6400, 6411), // &Oti -> &Otim + new Transition (6406, 6415), // &oti -> &otim + new Transition (6422, 6423), // &Ou -> &Oum + new Transition (6426, 6427), // &ou -> &oum + new Transition (6463, 6607), // &p -> &pm + new Transition (6475, 6476), // &parsi -> &parsim + new Transition (6498, 6507), // &per -> &perm + new Transition (6527, 6532), // &ph -> &phm + new Transition (6532, 6533), // &phm -> &phmm + new Transition (6567, 6596), // &plus -> &plusm + new Transition (6600, 6601), // &plussi -> &plussim + new Transition (6718, 6719), // &precnsi -> &precnsim + new Transition (6722, 6723), // &precsi -> &precsim + new Transition (6725, 6726), // &Pri -> &Prim + new Transition (6729, 6730), // &pri -> &prim + new Transition (6742, 6743), // &prnsi -> &prnsim + new Transition (6787, 6788), // &prsi -> &prsim + new Transition (6835, 6836), // &qpri -> &qprim + new Transition (6876, 7453), // &r -> &rm + new Transition (6901, 6902), // &rae -> &raem + new Transition (6957, 6958), // &rarrsi -> &rarrsim + new Transition (7104, 7105), // &ReverseEle -> &ReverseElem + new Transition (7118, 7119), // &ReverseEquilibriu -> &ReverseEquilibrium + new Transition (7132, 7133), // &ReverseUpEquilibriu -> &ReverseUpEquilibrium + new Transition (7360, 7361), // &rightthreeti -> &rightthreetim + new Transition (7442, 7451), // &rl -> &rlm + new Transition (7464, 7465), // &rn -> &rnm + new Transition (7496, 7497), // &roti -> &rotim + new Transition (7504, 7505), // &RoundI -> &RoundIm + new Transition (7573, 7574), // &rti -> &rtim + new Transition (7610, 7883), // &S -> &Sm + new Transition (7617, 7894), // &s -> &sm + new Transition (7677, 7678), // &scnsi -> &scnsim + new Transition (7688, 7689), // &scsi -> &scsim + new Transition (7703, 7721), // &se -> &sem + new Transition (7729, 7730), // &set -> &setm + new Transition (7798, 7799), // &short -> &shortm + new Transition (7834, 7835), // &Sig -> &Sigm + new Transition (7838, 7847), // &si -> &sim + new Transition (7839, 7840), // &sig -> &sigm + new Transition (7900, 7901), // &smallset -> &smallsetm + new Transition (8077, 8086), // &ss -> &ssm + new Transition (8082, 8083), // &sset -> &ssetm + new Transition (8127, 8275), // &Su -> &Sum + new Transition (8130, 8277), // &su -> &sum + new Transition (8131, 8145), // &sub -> &subm + new Transition (8190, 8191), // &subsi -> &subsim + new Transition (8262, 8263), // &succnsi -> &succnsim + new Transition (8266, 8267), // &succsi -> &succsim + new Transition (8284, 8333), // &sup -> &supm + new Transition (8367, 8368), // &supsi -> &supsim + new Transition (8488, 8489), // &thetasy -> &thetasym + new Transition (8504, 8505), // &thicksi -> &thicksim + new Transition (8532, 8533), // &thksi -> &thksim + new Transition (8549, 8576), // &ti -> &tim + new Transition (8619, 8620), // &tpri -> &tprim + new Transition (8633, 8670), // &tri -> &trim + new Transition (8694, 8695), // &triti -> &tritim + new Transition (8702, 8703), // &trpeziu -> &trpezium + new Transition (8768, 8904), // &U -> &Um + new Transition (8775, 8909), // &u -> &um + new Transition (9043, 9044), // &UpEquilibriu -> &UpEquilibrium + new Transition (9182, 9191), // &uu -> &uum + new Transition (9187, 9188), // &Uu -> &Uum + new Transition (9254, 9255), // &varsig -> &varsigm + new Transition (9548, 9594), // &x -> &xm + new Transition (9619, 9620), // &xoti -> &xotim + new Transition (9736, 9744), // &yu -> &yum + new Transition (9740, 9741) // &Yu -> &Yum + }; + TransitionTable_n = new Transition[303] { + new Transition (0, 4965), // & -> &n + new Transition (1, 116), // &A -> &An + new Transition (8, 119), // &a -> &an + new Transition (122, 123), // &anda -> &andan + new Transition (185, 186), // &Aogo -> &Aogon + new Transition (190, 191), // &aogo -> &aogon + new Transition (221, 222), // &ApplyFu -> &ApplyFun + new Transition (226, 227), // &ApplyFunctio -> &ApplyFunction + new Transition (238, 239), // &Ari -> &Arin + new Transition (243, 244), // &ari -> &arin + new Transition (257, 258), // &Assig -> &Assign + new Transition (291, 292), // &awco -> &awcon + new Transition (293, 294), // &awconi -> &awconin + new Transition (297, 298), // &awi -> &awin + new Transition (301, 579), // &b -> &bn + new Transition (306, 307), // &backco -> &backcon + new Transition (315, 316), // &backepsilo -> &backepsilon + new Transition (370, 371), // &bco -> &bcon + new Transition (409, 410), // &ber -> &bern + new Transition (414, 415), // &Ber -> &Bern + new Transition (433, 434), // &betwee -> &between + new Transition (484, 485), // &bigtria -> &bigtrian + new Transition (491, 492), // &bigtriangledow -> &bigtriangledown + new Transition (520, 563), // &bla -> &blan + new Transition (526, 527), // &blackloze -> &blacklozen + new Transition (541, 542), // &blacktria -> &blacktrian + new Transition (549, 550), // &blacktriangledow -> &blacktriangledown + new Transition (657, 658), // &boxmi -> &boxmin + new Transition (807, 808), // &capa -> &capan + new Transition (838, 839), // &CapitalDiffere -> &CapitalDifferen + new Transition (852, 853), // &caro -> &caron + new Transition (869, 870), // &Ccaro -> &Ccaron + new Transition (873, 874), // &ccaro -> &ccaron + new Transition (894, 895), // &Cco -> &Ccon + new Transition (896, 897), // &Cconi -> &Cconin + new Transition (915, 933), // &ce -> &cen + new Transition (920, 936), // &Ce -> &Cen + new Transition (1033, 1034), // &CircleMi -> &CircleMin + new Transition (1053, 1054), // &cirf -> &cirfn + new Transition (1055, 1056), // &cirfni -> &cirfnin + new Transition (1077, 1078), // &ClockwiseCo -> &ClockwiseCon + new Transition (1083, 1084), // &ClockwiseContourI -> &ClockwiseContourIn + new Transition (1126, 1171), // &Co -> &Con + new Transition (1128, 1129), // &Colo -> &Colon + new Transition (1131, 1164), // &co -> &con + new Transition (1133, 1134), // &colo -> &colon + new Transition (1150, 1151), // &compf -> &compfn + new Transition (1156, 1157), // &compleme -> &complemen + new Transition (1175, 1176), // &Congrue -> &Congruen + new Transition (1179, 1180), // &Coni -> &Conin + new Transition (1183, 1184), // &coni -> &conin + new Transition (1191, 1192), // &ContourI -> &ContourIn + new Transition (1226, 1227), // &Cou -> &Coun + new Transition (1241, 1242), // &CounterClockwiseCo -> &CounterClockwiseCon + new Transition (1247, 1248), // &CounterClockwiseContourI -> &CounterClockwiseContourIn + new Transition (1378, 1379), // &curre -> ¤ + new Transition (1409, 1410), // &cwco -> &cwcon + new Transition (1411, 1412), // &cwconi -> &cwconin + new Transition (1415, 1416), // &cwi -> &cwin + new Transition (1477, 1478), // &Dcaro -> &Dcaron + new Transition (1483, 1484), // &dcaro -> &dcaron + new Transition (1604, 1605), // &Diamo -> &Diamon + new Transition (1608, 1609), // &diamo -> &diamon + new Transition (1625, 1626), // &Differe -> &Differen + new Transition (1640, 1641), // &disi -> &disin + new Transition (1649, 1650), // ÷o -> ÷on + new Transition (1657, 1658), // &divo -> &divon + new Transition (1672, 1673), // &dlcor -> &dlcorn + new Transition (1714, 1715), // &dotmi -> &dotmin + new Transition (1749, 1750), // &DoubleCo -> &DoubleCon + new Transition (1755, 1756), // &DoubleContourI -> &DoubleContourIn + new Transition (1768, 1769), // &DoubleDow -> &DoubleDown + new Transition (1801, 1802), // &DoubleLo -> &DoubleLon + new Transition (1861, 1862), // &DoubleUpDow -> &DoubleUpDown + new Transition (1881, 1882), // &Dow -> &Down + new Transition (1895, 1896), // &dow -> &down + new Transition (1923, 1924), // &downdow -> &downdown + new Transition (1937, 1938), // &downharpoo -> &downharpoon + new Transition (2033, 2034), // &drcor -> &drcorn + new Transition (2087, 2088), // &dwa -> &dwan + new Transition (2115, 2290), // &e -> &en + new Transition (2130, 2131), // &Ecaro -> &Ecaron + new Transition (2136, 2137), // &ecaro -> &ecaron + new Transition (2150, 2151), // &ecolo -> &ecolon + new Transition (2209, 2210), // &Eleme -> &Elemen + new Transition (2213, 2214), // &eli -> &elin + new Transition (2298, 2299), // &Eogo -> &Eogon + new Transition (2303, 2304), // &eogo -> &eogon + new Transition (2330, 2331), // &Epsilo -> &Epsilon + new Transition (2334, 2335), // &epsilo -> &epsilon + new Transition (2347, 2348), // &eqcolo -> &eqcolon + new Transition (2355, 2356), // &eqsla -> &eqslan + new Transition (2479, 2480), // &expectatio -> &expectation + new Transition (2483, 2484), // &Expo -> &Expon + new Transition (2485, 2486), // &Expone -> &Exponen + new Transition (2493, 2494), // &expo -> &expon + new Transition (2495, 2496), // &expone -> &exponen + new Transition (2503, 2604), // &f -> &fn + new Transition (2507, 2508), // &falli -> &fallin + new Transition (2600, 2601), // &flt -> &fltn + new Transition (2643, 2644), // &fparti -> &fpartin + new Transition (2690, 2691), // &frow -> &frown + new Transition (2701, 2832), // &g -> &gn + new Transition (2777, 2778), // &geqsla -> &geqslan + new Transition (2908, 2909), // &GreaterSla -> &GreaterSlan + new Transition (3002, 3011), // &gv -> &gvn + new Transition (3005, 3006), // &gvert -> &gvertn + new Transition (3091, 3092), // &herco -> &hercon + new Transition (3174, 3175), // &Horizo -> &Horizon + new Transition (3180, 3181), // &HorizontalLi -> &HorizontalLin + new Transition (3212, 3213), // &HumpDow -> &HumpDown + new Transition (3233, 3234), // &hyphe -> &hyphen + new Transition (3236, 3398), // &I -> &In + new Transition (3243, 3378), // &i -> &in + new Transition (3301, 3311), // &ii -> &iin + new Transition (3303, 3308), // &iii -> &iiin + new Transition (3304, 3305), // &iiii -> &iiiin + new Transition (3313, 3314), // &iinfi -> &iinfin + new Transition (3345, 3346), // &Imagi -> &Imagin + new Transition (3353, 3354), // &imagli -> &imaglin + new Transition (3386, 3387), // &infi -> &infin + new Transition (3430, 3431), // &Intersectio -> &Intersection + new Transition (3473, 3474), // &Iogo -> &Iogon + new Transition (3477, 3478), // &iogo -> &iogon + new Transition (3511, 3512), // &isi -> &isin + new Transition (3657, 3658), // &kgree -> &kgreen + new Transition (3692, 4401), // &l -> &ln + new Transition (3699, 3733), // &La -> &Lan + new Transition (3705, 3736), // &la -> &lan + new Transition (3720, 3721), // &lagra -> &lagran + new Transition (3840, 3841), // &Lcaro -> &Lcaron + new Transition (3846, 3847), // &lcaro -> &lcaron + new Transition (3901, 3902), // &LeftA -> &LeftAn + new Transition (3957, 3958), // &LeftCeili -> &LeftCeilin + new Transition (3975, 3976), // &LeftDow -> &LeftDown + new Transition (4009, 4010), // &leftharpoo -> &leftharpoon + new Transition (4013, 4014), // &leftharpoondow -> &leftharpoondown + new Transition (4070, 4071), // &leftrightharpoo -> &leftrightharpoon + new Transition (4122, 4123), // &LeftTria -> &LeftTrian + new Transition (4142, 4143), // &LeftUpDow -> &LeftUpDown + new Transition (4193, 4194), // &leqsla -> &leqslan + new Transition (4286, 4287), // &LessSla -> &LessSlan + new Transition (4356, 4357), // &llcor -> &llcorn + new Transition (4422, 4457), // &lo -> &lon + new Transition (4423, 4424), // &loa -> &loan + new Transition (4434, 4435), // &Lo -> &Lon + new Transition (4614, 4615), // &loze -> &lozen + new Transition (4635, 4636), // &lrcor -> &lrcorn + new Transition (4755, 4764), // &lv -> &lvn + new Transition (4758, 4759), // &lvert -> &lvertn + new Transition (4767, 4916), // &m -> &mn + new Transition (4793, 4794), // &mapstodow -> &mapstodown + new Transition (4837, 4838), // &measureda -> &measuredan + new Transition (4856, 4857), // &Melli -> &Mellin + new Transition (4871, 4890), // &mi -> &min + new Transition (4900, 4901), // &Mi -> &Min + new Transition (4966, 4983), // &na -> &nan + new Transition (5027, 5028), // &Ncaro -> &Ncaron + new Transition (5031, 5032), // &ncaro -> &ncaron + new Transition (5044, 5045), // &nco -> &ncon + new Transition (5105, 5114), // &NegativeThi -> &NegativeThin + new Transition (5127, 5128), // &NegativeVeryThi -> &NegativeVeryThin + new Transition (5178, 5179), // &NewLi -> &NewLin + new Transition (5206, 5207), // &ngeqsla -> &ngeqslan + new Transition (5318, 5319), // &nleqsla -> &nleqslan + new Transition (5347, 5354), // &No -> &Non + new Transition (5360, 5361), // &NonBreaki -> &NonBreakin + new Transition (5378, 5620), // ¬ -> ¬n + new Transition (5381, 5382), // &NotCo -> &NotCon + new Transition (5386, 5387), // &NotCongrue -> &NotCongruen + new Transition (5418, 5419), // &NotEleme -> &NotElemen + new Transition (5478, 5479), // &NotGreaterSla -> &NotGreaterSlan + new Transition (5499, 5500), // &NotHumpDow -> &NotHumpDown + new Transition (5512, 5513), // ¬i -> ¬in + new Transition (5535, 5536), // &NotLeftTria -> &NotLeftTrian + new Transition (5575, 5576), // &NotLessSla -> &NotLessSlan + new Transition (5647, 5648), // &NotPrecedesSla -> &NotPrecedesSlan + new Transition (5667, 5668), // &NotReverseEleme -> &NotReverseElemen + new Transition (5678, 5679), // &NotRightTria -> &NotRightTrian + new Transition (5753, 5754), // &NotSucceedsSla -> &NotSucceedsSlan + new Transition (5838, 5839), // &npoli -> &npolin + new Transition (6008, 6009), // &ntria -> &ntrian + new Transition (6078, 6079), // &nvi -> &nvin + new Transition (6081, 6082), // &nvinfi -> &nvinfin + new Transition (6111, 6126), // &nw -> &nwn + new Transition (6211, 6212), // &ogo -> &ogon + new Transition (6234, 6235), // &oi -> &oin + new Transition (6252, 6253), // &oli -> &olin + new Transition (6279, 6280), // &Omicro -> &Omicron + new Transition (6282, 6290), // &omi -> &omin + new Transition (6285, 6286), // &omicro -> &omicron + new Transition (6307, 6308), // &Ope -> &Open + new Transition (6454, 6455), // &OverPare -> &OverParen + new Transition (6499, 6500), // &perc -> &percn + new Transition (6514, 6515), // &perte -> &perten + new Transition (6537, 6538), // &pho -> &phon + new Transition (6556, 6557), // &pla -> &plan + new Transition (6591, 6592), // &PlusMi -> &PlusMin + new Transition (6596, 6597), // &plusm -> ± + new Transition (6610, 6611), // &Poi -> &Poin + new Transition (6618, 6619), // &Poincarepla -> &Poincareplan + new Transition (6623, 6624), // &poi -> &poin + new Transition (6626, 6627), // &pointi -> &pointin + new Transition (6636, 6637), // &pou -> &poun + new Transition (6642, 6735), // &pr -> &prn + new Transition (6655, 6705), // &prec -> &precn + new Transition (6687, 6688), // &PrecedesSla -> &PrecedesSlan + new Transition (6761, 6762), // &profli -> &proflin + new Transition (6777, 6778), // &Proportio -> &Proportion + new Transition (6807, 6808), // &pu -> &pun + new Transition (6821, 6822), // &qi -> &qin + new Transition (6851, 6852), // &quater -> &quatern + new Transition (6854, 6855), // &quaternio -> &quaternion + new Transition (6858, 6859), // &quati -> &quatin + new Transition (6876, 7464), // &r -> &rn + new Transition (6882, 6911), // &ra -> &ran + new Transition (6887, 6908), // &Ra -> &Ran + new Transition (6979, 6981), // &ratio -> &ration + new Transition (7024, 7025), // &Rcaro -> &Rcaron + new Transition (7030, 7031), // &rcaro -> &rcaron + new Transition (7078, 7079), // &reali -> &realin + new Transition (7106, 7107), // &ReverseEleme -> &ReverseElemen + new Transition (7175, 7176), // &RightA -> &RightAn + new Transition (7199, 7428), // &ri -> &rin + new Transition (7232, 7233), // &RightCeili -> &RightCeilin + new Transition (7250, 7251), // &RightDow -> &RightDown + new Transition (7284, 7285), // &rightharpoo -> &rightharpoon + new Transition (7288, 7289), // &rightharpoondow -> &rightharpoondown + new Transition (7310, 7311), // &rightleftharpoo -> &rightleftharpoon + new Transition (7367, 7368), // &RightTria -> &RightTrian + new Transition (7387, 7388), // &RightUpDow -> &RightUpDown + new Transition (7432, 7433), // &risi -> &risin + new Transition (7470, 7471), // &roa -> &roan + new Transition (7501, 7502), // &Rou -> &Roun + new Transition (7522, 7523), // &rppoli -> &rppolin + new Transition (7631, 7670), // &sc -> &scn + new Transition (7638, 7639), // &Scaro -> &Scaron + new Transition (7642, 7643), // &scaro -> &scaron + new Transition (7683, 7684), // &scpoli -> &scpolin + new Transition (7730, 7736), // &setm -> &setmn + new Transition (7731, 7732), // &setmi -> &setmin + new Transition (7748, 7749), // &sfrow -> &sfrown + new Transition (7778, 7779), // &ShortDow -> &ShortDown + new Transition (7847, 7865), // &sim -> &simn + new Transition (7902, 7903), // &smallsetmi -> &smallsetmin + new Transition (8019, 8020), // &SquareI -> &SquareIn + new Transition (8029, 8030), // &SquareIntersectio -> &SquareIntersection + new Transition (8058, 8059), // &SquareU -> &SquareUn + new Transition (8061, 8062), // &SquareUnio -> &SquareUnion + new Transition (8083, 8084), // &ssetm -> &ssetmn + new Transition (8106, 8124), // &str -> &strn + new Transition (8117, 8118), // &straightepsilo -> &straightepsilon + new Transition (8130, 8279), // &su -> &sun + new Transition (8131, 8150), // &sub -> &subn + new Transition (8171, 8184), // &subset -> &subsetn + new Transition (8199, 8249), // &succ -> &succn + new Transition (8231, 8232), // &SucceedsSla -> &SucceedsSlan + new Transition (8284, 8338), // &sup -> &supn + new Transition (8354, 8361), // &supset -> &supsetn + new Transition (8375, 8390), // &sw -> &swn + new Transition (8422, 8423), // &Tcaro -> &Tcaron + new Transition (8428, 8429), // &tcaro -> &tcaron + new Transition (8493, 8516), // &thi -> &thin + new Transition (8507, 8520), // &Thi -> &Thin + new Transition (8541, 8542), // &thor -> þ + new Transition (8549, 8587), // &ti -> &tin + new Transition (8634, 8635), // &tria -> &trian + new Transition (8642, 8643), // &triangledow -> &triangledown + new Transition (8671, 8672), // &trimi -> &trimin + new Transition (8768, 8916), // &U -> &Un + new Transition (8890, 8891), // &ulcor -> &ulcorn + new Transition (8936, 8937), // &UnderPare -> &UnderParen + new Transition (8946, 8947), // &Unio -> &Union + new Transition (8956, 8957), // &Uogo -> &Uogon + new Transition (8961, 8962), // &uogo -> &uogon + new Transition (8996, 8997), // &UpArrowDow -> &UpArrowDown + new Transition (9006, 9007), // &UpDow -> &UpDown + new Transition (9016, 9017), // &Updow -> &Updown + new Transition (9026, 9027), // &updow -> &updown + new Transition (9051, 9052), // &upharpoo -> &upharpoon + new Transition (9101, 9102), // &Upsilo -> &Upsilon + new Transition (9105, 9106), // &upsilo -> &upsilon + new Transition (9130, 9131), // &urcor -> &urcorn + new Transition (9141, 9142), // &Uri -> &Urin + new Transition (9145, 9146), // &uri -> &urin + new Transition (9195, 9196), // &uwa -> &uwan + new Transition (9201, 9425), // &v -> &vn + new Transition (9202, 9203), // &va -> &van + new Transition (9208, 9223), // &var -> &varn + new Transition (9214, 9215), // &varepsilo -> &varepsilon + new Transition (9227, 9228), // &varnothi -> &varnothin + new Transition (9262, 9263), // &varsubset -> &varsubsetn + new Transition (9272, 9273), // &varsupset -> &varsupsetn + new Transition (9287, 9288), // &vartria -> &vartrian + new Transition (9383, 9384), // &VerticalLi -> &VerticalLin + new Transition (9406, 9407), // &VeryThi -> &VeryThin + new Transition (9459, 9460), // &vsub -> &vsubn + new Transition (9465, 9466), // &vsup -> &vsupn + new Transition (9548, 9598), // &x -> &xn + new Transition (9699, 9700), // &ye -> ¥ + new Transition (9764, 9765), // &Zcaro -> &Zcaron + new Transition (9770, 9771), // &zcaro -> &zcaron + new Transition (9848, 9851) // &zw -> &zwn + }; + TransitionTable_o = new Transition[460] { + new Transition (0, 6138), // & -> &o + new Transition (1, 183), // &A -> &Ao + new Transition (8, 188), // &a -> &ao + new Transition (129, 130), // &andsl -> &andslo + new Transition (184, 185), // &Aog -> &Aogo + new Transition (189, 190), // &aog -> &aogo + new Transition (199, 213), // &ap -> &apo + new Transition (225, 226), // &ApplyFuncti -> &ApplyFunctio + new Transition (230, 231), // &appr -> &appro + new Transition (290, 291), // &awc -> &awco + new Transition (301, 598), // &b -> &bo + new Transition (305, 306), // &backc -> &backco + new Transition (314, 315), // &backepsil -> &backepsilo + new Transition (331, 594), // &B -> &Bo + new Transition (369, 370), // &bc -> &bco + new Transition (381, 382), // &bdqu -> &bdquo + new Transition (410, 411), // &bern -> &berno + new Transition (415, 416), // &Bern -> &Berno + new Transition (443, 455), // &big -> &bigo + new Transition (456, 457), // &bigod -> &bigodo + new Transition (489, 490), // &bigtriangled -> &bigtriangledo + new Transition (515, 516), // &bkar -> &bkaro + new Transition (519, 575), // &bl -> &blo + new Transition (523, 524), // &blackl -> &blacklo + new Transition (547, 548), // &blacktriangled -> &blacktriangledo + new Transition (579, 591), // &bn -> &bno + new Transition (587, 588), // &bN -> &bNo + new Transition (604, 605), // &bott -> &botto + new Transition (614, 615), // &boxb -> &boxbo + new Transition (744, 757), // &bs -> &bso + new Transition (789, 1126), // &C -> &Co + new Transition (796, 1131), // &c -> &co + new Transition (824, 825), // &capd -> &capdo + new Transition (848, 852), // &car -> &caro + new Transition (866, 894), // &Cc -> &Cco + new Transition (868, 869), // &Ccar -> &Ccaro + new Transition (872, 873), // &ccar -> &ccaro + new Transition (907, 908), // &Cd -> &Cdo + new Transition (911, 912), // &cd -> &cdo + new Transition (940, 941), // &CenterD -> &CenterDo + new Transition (946, 947), // ¢erd -> ¢erdo + new Transition (990, 991), // &circlearr -> &circlearro + new Transition (1024, 1025), // &CircleD -> &CircleDo + new Transition (1068, 1069), // &Cl -> &Clo + new Transition (1076, 1077), // &ClockwiseC -> &ClockwiseCo + new Transition (1079, 1080), // &ClockwiseCont -> &ClockwiseConto + new Transition (1099, 1100), // &CloseCurlyD -> &CloseCurlyDo + new Transition (1106, 1107), // &CloseCurlyDoubleQu -> &CloseCurlyDoubleQuo + new Transition (1112, 1113), // &CloseCurlyQu -> &CloseCurlyQuo + new Transition (1127, 1128), // &Col -> &Colo + new Transition (1132, 1133), // &col -> &colo + new Transition (1167, 1168), // &congd -> &congdo + new Transition (1187, 1188), // &Cont -> &Conto + new Transition (1206, 1207), // &copr -> &copro + new Transition (1210, 1211), // &Copr -> &Copro + new Transition (1232, 1233), // &CounterCl -> &CounterClo + new Transition (1240, 1241), // &CounterClockwiseC -> &CounterClockwiseCo + new Transition (1243, 1244), // &CounterClockwiseCont -> &CounterClockwiseConto + new Transition (1256, 1266), // &cr -> &cro + new Transition (1261, 1262), // &Cr -> &Cro + new Transition (1288, 1289), // &ctd -> &ctdo + new Transition (1318, 1341), // &cup -> &cupo + new Transition (1337, 1338), // &cupd -> &cupdo + new Transition (1385, 1386), // &curvearr -> &curvearro + new Transition (1408, 1409), // &cwc -> &cwco + new Transition (1425, 1685), // &D -> &Do + new Transition (1432, 1679), // &d -> &do + new Transition (1466, 1467), // &dbkar -> &dbkaro + new Transition (1476, 1477), // &Dcar -> &Dcaro + new Transition (1482, 1483), // &dcar -> &dcaro + new Transition (1490, 1503), // &DD -> &DDo + new Transition (1492, 1510), // &dd -> &ddo + new Transition (1573, 1574), // &DiacriticalD -> &DiacriticalDo + new Transition (1601, 1608), // &diam -> &diamo + new Transition (1603, 1604), // &Diam -> &Diamo + new Transition (1643, 1657), // &div -> &divo + new Transition (1647, 1649), // ÷ -> ÷o + new Transition (1670, 1671), // &dlc -> &dlco + new Transition (1675, 1676), // &dlcr -> &dlcro + new Transition (1696, 1697), // &DotD -> &DotDo + new Transition (1703, 1704), // &doteqd -> &doteqdo + new Transition (1748, 1749), // &DoubleC -> &DoubleCo + new Transition (1751, 1752), // &DoubleCont -> &DoubleConto + new Transition (1764, 1765), // &DoubleD -> &DoubleDo + new Transition (1772, 1773), // &DoubleDownArr -> &DoubleDownArro + new Transition (1776, 1801), // &DoubleL -> &DoubleLo + new Transition (1782, 1783), // &DoubleLeftArr -> &DoubleLeftArro + new Transition (1793, 1794), // &DoubleLeftRightArr -> &DoubleLeftRightArro + new Transition (1810, 1811), // &DoubleLongLeftArr -> &DoubleLongLeftArro + new Transition (1821, 1822), // &DoubleLongLeftRightArr -> &DoubleLongLeftRightArro + new Transition (1832, 1833), // &DoubleLongRightArr -> &DoubleLongRightArro + new Transition (1843, 1844), // &DoubleRightArr -> &DoubleRightArro + new Transition (1855, 1856), // &DoubleUpArr -> &DoubleUpArro + new Transition (1859, 1860), // &DoubleUpD -> &DoubleUpDo + new Transition (1865, 1866), // &DoubleUpDownArr -> &DoubleUpDownArro + new Transition (1885, 1886), // &DownArr -> &DownArro + new Transition (1891, 1892), // &Downarr -> &Downarro + new Transition (1899, 1900), // &downarr -> &downarro + new Transition (1911, 1912), // &DownArrowUpArr -> &DownArrowUpArro + new Transition (1921, 1922), // &downd -> &downdo + new Transition (1927, 1928), // &downdownarr -> &downdownarro + new Transition (1935, 1936), // &downharp -> &downharpo + new Transition (1936, 1937), // &downharpo -> &downharpoo + new Transition (1962, 1963), // &DownLeftRightVect -> &DownLeftRightVecto + new Transition (1972, 1973), // &DownLeftTeeVect -> &DownLeftTeeVecto + new Transition (1979, 1980), // &DownLeftVect -> &DownLeftVecto + new Transition (1998, 1999), // &DownRightTeeVect -> &DownRightTeeVecto + new Transition (2005, 2006), // &DownRightVect -> &DownRightVecto + new Transition (2019, 2020), // &DownTeeArr -> &DownTeeArro + new Transition (2027, 2028), // &drbkar -> &drbkaro + new Transition (2031, 2032), // &drc -> &drco + new Transition (2036, 2037), // &drcr -> &drcro + new Transition (2044, 2054), // &ds -> &dso + new Transition (2058, 2059), // &Dstr -> &Dstro + new Transition (2063, 2064), // &dstr -> &dstro + new Transition (2068, 2069), // &dtd -> &dtdo + new Transition (2108, 2296), // &E -> &Eo + new Transition (2115, 2301), // &e -> &eo + new Transition (2129, 2130), // &Ecar -> &Ecaro + new Transition (2133, 2148), // &ec -> &eco + new Transition (2135, 2136), // &ecar -> &ecaro + new Transition (2149, 2150), // &ecol -> &ecolo + new Transition (2157, 2166), // &eD -> &eDo + new Transition (2158, 2159), // &eDD -> &eDDo + new Transition (2162, 2163), // &Ed -> &Edo + new Transition (2169, 2170), // &ed -> &edo + new Transition (2176, 2177), // &efD -> &efDo + new Transition (2200, 2201), // &egsd -> &egsdo + new Transition (2224, 2225), // &elsd -> &elsdo + new Transition (2297, 2298), // &Eog -> &Eogo + new Transition (2302, 2303), // &eog -> &eogo + new Transition (2329, 2330), // &Epsil -> &Epsilo + new Transition (2333, 2334), // &epsil -> &epsilo + new Transition (2340, 2345), // &eqc -> &eqco + new Transition (2346, 2347), // &eqcol -> &eqcolo + new Transition (2414, 2415), // &erD -> &erDo + new Transition (2426, 2427), // &esd -> &esdo + new Transition (2455, 2456), // &eur -> &euro + new Transition (2472, 2493), // &exp -> &expo + new Transition (2478, 2479), // &expectati -> &expectatio + new Transition (2482, 2483), // &Exp -> &Expo + new Transition (2503, 2612), // &f -> &fo + new Transition (2510, 2511), // &fallingd -> &fallingdo + new Transition (2517, 2608), // &F -> &Fo + new Transition (2604, 2605), // &fn -> &fno + new Transition (2647, 2689), // &fr -> &fro + new Transition (2701, 2857), // &g -> &go + new Transition (2708, 2853), // &G -> &Go + new Transition (2755, 2756), // &Gd -> &Gdo + new Transition (2759, 2760), // &gd -> &gdo + new Transition (2786, 2787), // &gesd -> &gesdo + new Transition (2788, 2790), // &gesdot -> &gesdoto + new Transition (2837, 2838), // &gnappr -> &gnappro + new Transition (2950, 2951), // >d -> >do + new Transition (2969, 2970), // >rappr -> >rappro + new Transition (2976, 2977), // >rd -> >rdo + new Transition (3014, 3159), // &H -> &Ho + new Transition (3020, 3126), // &h -> &ho + new Transition (3090, 3091), // &herc -> &herco + new Transition (3116, 3117), // &hksear -> &hksearo + new Transition (3122, 3123), // &hkswar -> &hkswaro + new Transition (3126, 3136), // &ho -> &hoo + new Transition (3144, 3145), // &hookleftarr -> &hookleftarro + new Transition (3155, 3156), // &hookrightarr -> &hookrightarro + new Transition (3173, 3174), // &Horiz -> &Horizo + new Transition (3198, 3199), // &Hstr -> &Hstro + new Transition (3203, 3204), // &hstr -> &hstro + new Transition (3210, 3211), // &HumpD -> &HumpDo + new Transition (3236, 3471), // &I -> &Io + new Transition (3243, 3467), // &i -> &io + new Transition (3265, 3266), // &Id -> &Ido + new Transition (3301, 3316), // &ii -> &iio + new Transition (3336, 3365), // &im -> &imo + new Transition (3378, 3393), // &in -> &ino + new Transition (3394, 3395), // &inod -> &inodo + new Transition (3429, 3430), // &Intersecti -> &Intersectio + new Transition (3440, 3441), // &intpr -> &intpro + new Transition (3451, 3452), // &InvisibleC -> &InvisibleCo + new Transition (3472, 3473), // &Iog -> &Iogo + new Transition (3476, 3477), // &iog -> &iogo + new Transition (3493, 3494), // &ipr -> &ipro + new Transition (3514, 3515), // &isind -> &isindo + new Transition (3555, 3582), // &J -> &Jo + new Transition (3561, 3586), // &j -> &jo + new Transition (3618, 3676), // &K -> &Ko + new Transition (3624, 3680), // &k -> &ko + new Transition (3692, 4422), // &l -> &lo + new Transition (3698, 4434), // &L -> &Lo + new Transition (3756, 3757), // &laqu -> « + new Transition (3839, 3840), // &Lcar -> &Lcaro + new Transition (3845, 3846), // &lcar -> &lcaro + new Transition (3874, 3875), // &ldqu -> &ldquo + new Transition (3915, 3916), // &LeftArr -> &LeftArro + new Transition (3921, 3922), // &Leftarr -> &Leftarro + new Transition (3929, 3930), // &leftarr -> &leftarro + new Transition (3944, 3945), // &LeftArrowRightArr -> &LeftArrowRightArro + new Transition (3961, 3962), // &LeftD -> &LeftDo + new Transition (3983, 3984), // &LeftDownTeeVect -> &LeftDownTeeVecto + new Transition (3990, 3991), // &LeftDownVect -> &LeftDownVecto + new Transition (3999, 4000), // &LeftFl -> &LeftFlo + new Transition (4000, 4001), // &LeftFlo -> &LeftFloo + new Transition (4007, 4008), // &leftharp -> &leftharpo + new Transition (4008, 4009), // &leftharpo -> &leftharpoo + new Transition (4011, 4012), // &leftharpoond -> &leftharpoondo + new Transition (4025, 4026), // &leftleftarr -> &leftleftarro + new Transition (4037, 4038), // &LeftRightArr -> &LeftRightArro + new Transition (4048, 4049), // &Leftrightarr -> &Leftrightarro + new Transition (4059, 4060), // &leftrightarr -> &leftrightarro + new Transition (4068, 4069), // &leftrightharp -> &leftrightharpo + new Transition (4069, 4070), // &leftrightharpo -> &leftrightharpoo + new Transition (4081, 4082), // &leftrightsquigarr -> &leftrightsquigarro + new Transition (4088, 4089), // &LeftRightVect -> &LeftRightVecto + new Transition (4098, 4099), // &LeftTeeArr -> &LeftTeeArro + new Transition (4105, 4106), // &LeftTeeVect -> &LeftTeeVecto + new Transition (4140, 4141), // &LeftUpD -> &LeftUpDo + new Transition (4147, 4148), // &LeftUpDownVect -> &LeftUpDownVecto + new Transition (4157, 4158), // &LeftUpTeeVect -> &LeftUpTeeVecto + new Transition (4164, 4165), // &LeftUpVect -> &LeftUpVecto + new Transition (4175, 4176), // &LeftVect -> &LeftVecto + new Transition (4202, 4203), // &lesd -> &lesdo + new Transition (4204, 4206), // &lesdot -> &lesdoto + new Transition (4219, 4220), // &lessappr -> &lessappro + new Transition (4223, 4224), // &lessd -> &lessdo + new Transition (4307, 4308), // &lfl -> &lflo + new Transition (4308, 4309), // &lflo -> &lfloo + new Transition (4354, 4355), // &llc -> &llco + new Transition (4366, 4367), // &Lleftarr -> &Lleftarro + new Transition (4381, 4382), // &Lmid -> &Lmido + new Transition (4385, 4391), // &lm -> &lmo + new Transition (4387, 4388), // &lmid -> &lmido + new Transition (4406, 4407), // &lnappr -> &lnappro + new Transition (4422, 4542), // &lo -> &loo + new Transition (4443, 4444), // &LongLeftArr -> &LongLeftArro + new Transition (4453, 4454), // &Longleftarr -> &Longleftarro + new Transition (4465, 4466), // &longleftarr -> &longleftarro + new Transition (4476, 4477), // &LongLeftRightArr -> &LongLeftRightArro + new Transition (4487, 4488), // &Longleftrightarr -> &Longleftrightarro + new Transition (4498, 4499), // &longleftrightarr -> &longleftrightarro + new Transition (4506, 4507), // &longmapst -> &longmapsto + new Transition (4516, 4517), // &LongRightArr -> &LongRightArro + new Transition (4527, 4528), // &Longrightarr -> &Longrightarro + new Transition (4538, 4539), // &longrightarr -> &longrightarro + new Transition (4546, 4547), // &looparr -> &looparro + new Transition (4597, 4598), // &LowerLeftArr -> &LowerLeftArro + new Transition (4608, 4609), // &LowerRightArr -> &LowerRightArro + new Transition (4633, 4634), // &lrc -> &lrco + new Transition (4655, 4656), // &lsaqu -> &lsaquo + new Transition (4679, 4680), // &lsqu -> &lsquo + new Transition (4685, 4686), // &Lstr -> &Lstro + new Transition (4690, 4691), // &lstr -> &lstro + new Transition (4706, 4707), // <d -> <do + new Transition (4767, 4922), // &m -> &mo + new Transition (4781, 4928), // &M -> &Mo + new Transition (4788, 4789), // &mapst -> &mapsto + new Transition (4791, 4792), // &mapstod -> &mapstodo + new Transition (4809, 4810), // &mc -> &mco + new Transition (4826, 4827), // &mDD -> &mDDo + new Transition (4868, 4869), // &mh -> &mho + new Transition (4873, 4874), // &micr -> µ + new Transition (4886, 4887), // &midd -> &middo + new Transition (4946, 4947), // &mstp -> &mstpo + new Transition (4965, 5372), // &n -> &no + new Transition (4971, 5347), // &N -> &No + new Transition (4986, 4993), // &nap -> &napo + new Transition (4997, 4998), // &nappr -> &nappro + new Transition (5020, 5044), // &nc -> &nco + new Transition (5026, 5027), // &Ncar -> &Ncaro + new Transition (5030, 5031), // &ncar -> &ncaro + new Transition (5048, 5049), // &ncongd -> &ncongdo + new Transition (5075, 5077), // &nearr -> &nearro + new Transition (5080, 5081), // &ned -> &nedo + new Transition (5278, 5279), // &nLeftarr -> &nLeftarro + new Transition (5286, 5287), // &nleftarr -> &nleftarro + new Transition (5297, 5298), // &nLeftrightarr -> &nLeftrightarro + new Transition (5308, 5309), // &nleftrightarr -> &nleftrightarro + new Transition (5380, 5381), // &NotC -> &NotCo + new Transition (5396, 5397), // &NotD -> &NotDo + new Transition (5497, 5498), // &NotHumpD -> &NotHumpDo + new Transition (5515, 5516), // ¬ind -> ¬indo + new Transition (5821, 5836), // &np -> &npo + new Transition (5875, 5876), // &nRightarr -> &nRightarro + new Transition (5885, 5886), // &nrightarr -> &nrightarro + new Transition (5910, 5911), // &nsh -> &nsho + new Transition (6037, 6038), // &numer -> &numero + new Transition (6121, 6123), // &nwarr -> &nwarro + new Transition (6131, 6294), // &O -> &Oo + new Transition (6138, 6298), // &o -> &oo + new Transition (6163, 6182), // &od -> &odo + new Transition (6185, 6186), // &ods -> &odso + new Transition (6210, 6211), // &og -> &ogo + new Transition (6247, 6248), // &olcr -> &olcro + new Transition (6278, 6279), // &Omicr -> &Omicro + new Transition (6284, 6285), // &omicr -> &omicro + new Transition (6314, 6315), // &OpenCurlyD -> &OpenCurlyDo + new Transition (6321, 6322), // &OpenCurlyDoubleQu -> &OpenCurlyDoubleQuo + new Transition (6327, 6328), // &OpenCurlyQu -> &OpenCurlyQuo + new Transition (6342, 6365), // &or -> &oro + new Transition (6351, 6353), // &order -> &ordero + new Transition (6361, 6362), // &orig -> &origo + new Transition (6369, 6370), // &orsl -> &orslo + new Transition (6382, 6396), // &os -> &oso + new Transition (6463, 6622), // &p -> &po + new Transition (6482, 6609), // &P -> &Po + new Transition (6503, 6504), // &peri -> &perio + new Transition (6527, 6537), // &ph -> &pho + new Transition (6548, 6549), // &pitchf -> &pitchfo + new Transition (6580, 6581), // &plusd -> &plusdo + new Transition (6604, 6605), // &plustw -> &plustwo + new Transition (6640, 6748), // &Pr -> &Pro + new Transition (6642, 6745), // &pr -> &pro + new Transition (6660, 6661), // &precappr -> &precappro + new Transition (6709, 6710), // &precnappr -> &precnappro + new Transition (6772, 6773), // &Prop -> &Propo + new Transition (6776, 6777), // &Proporti -> &Proportio + new Transition (6783, 6784), // &propt -> &propto + new Transition (6813, 6825), // &Q -> &Qo + new Transition (6817, 6829), // &q -> &qo + new Transition (6847, 6873), // &qu -> &quo + new Transition (6853, 6854), // &quaterni -> &quaternio + new Transition (6876, 7469), // &r -> &ro + new Transition (6886, 7485), // &R -> &Ro + new Transition (6922, 6923), // &raqu -> » + new Transition (6978, 6979), // &rati -> &ratio + new Transition (7023, 7024), // &Rcar -> &Rcaro + new Transition (7029, 7030), // &rcar -> &rcaro + new Transition (7064, 7065), // &rdqu -> &rdquo + new Transition (7141, 7142), // &rfl -> &rflo + new Transition (7142, 7143), // &rflo -> &rfloo + new Transition (7155, 7167), // &rh -> &rho + new Transition (7164, 7165), // &Rh -> &Rho + new Transition (7189, 7190), // &RightArr -> &RightArro + new Transition (7195, 7196), // &Rightarr -> &Rightarro + new Transition (7205, 7206), // &rightarr -> &rightarro + new Transition (7219, 7220), // &RightArrowLeftArr -> &RightArrowLeftArro + new Transition (7236, 7237), // &RightD -> &RightDo + new Transition (7258, 7259), // &RightDownTeeVect -> &RightDownTeeVecto + new Transition (7265, 7266), // &RightDownVect -> &RightDownVecto + new Transition (7274, 7275), // &RightFl -> &RightFlo + new Transition (7275, 7276), // &RightFlo -> &RightFloo + new Transition (7282, 7283), // &rightharp -> &rightharpo + new Transition (7283, 7284), // &rightharpo -> &rightharpoo + new Transition (7286, 7287), // &rightharpoond -> &rightharpoondo + new Transition (7300, 7301), // &rightleftarr -> &rightleftarro + new Transition (7308, 7309), // &rightleftharp -> &rightleftharpo + new Transition (7309, 7310), // &rightleftharpo -> &rightleftharpoo + new Transition (7321, 7322), // &rightrightarr -> &rightrightarro + new Transition (7333, 7334), // &rightsquigarr -> &rightsquigarro + new Transition (7343, 7344), // &RightTeeArr -> &RightTeeArro + new Transition (7350, 7351), // &RightTeeVect -> &RightTeeVecto + new Transition (7385, 7386), // &RightUpD -> &RightUpDo + new Transition (7392, 7393), // &RightUpDownVect -> &RightUpDownVecto + new Transition (7402, 7403), // &RightUpTeeVect -> &RightUpTeeVecto + new Transition (7409, 7410), // &RightUpVect -> &RightUpVecto + new Transition (7420, 7421), // &RightVect -> &RightVecto + new Transition (7435, 7436), // &risingd -> &risingdo + new Transition (7453, 7454), // &rm -> &rmo + new Transition (7519, 7520), // &rpp -> &rppo + new Transition (7538, 7539), // &Rrightarr -> &Rrightarro + new Transition (7545, 7546), // &rsaqu -> &rsaquo + new Transition (7562, 7563), // &rsqu -> &rsquo + new Transition (7610, 7949), // &S -> &So + new Transition (7617, 7936), // &s -> &so + new Transition (7626, 7627), // &sbqu -> &sbquo + new Transition (7637, 7638), // &Scar -> &Scaro + new Transition (7641, 7642), // &scar -> &scaro + new Transition (7680, 7681), // &scp -> &scpo + new Transition (7695, 7696), // &sd -> &sdo + new Transition (7713, 7715), // &searr -> &searro + new Transition (7745, 7747), // &sfr -> &sfro + new Transition (7751, 7796), // &sh -> &sho + new Transition (7772, 7773), // &Sh -> &Sho + new Transition (7776, 7777), // &ShortD -> &ShortDo + new Transition (7782, 7783), // &ShortDownArr -> &ShortDownArro + new Transition (7792, 7793), // &ShortLeftArr -> &ShortLeftArro + new Transition (7819, 7820), // &ShortRightArr -> &ShortRightArro + new Transition (7827, 7828), // &ShortUpArr -> &ShortUpArro + new Transition (7849, 7850), // &simd -> &simdo + new Transition (8028, 8029), // &SquareIntersecti -> &SquareIntersectio + new Transition (8060, 8061), // &SquareUni -> &SquareUnio + new Transition (8116, 8117), // &straightepsil -> &straightepsilo + new Transition (8133, 8134), // &subd -> &subdo + new Transition (8141, 8142), // &subed -> &subedo + new Transition (8204, 8205), // &succappr -> &succappro + new Transition (8253, 8254), // &succnappr -> &succnappro + new Transition (8292, 8293), // &supd -> &supdo + new Transition (8304, 8305), // &suped -> &supedo + new Transition (8321, 8322), // &suphs -> &suphso + new Transition (8385, 8387), // &swarr -> &swarro + new Transition (8400, 8604), // &T -> &To + new Transition (8404, 8590), // &t -> &to + new Transition (8421, 8422), // &Tcar -> &Tcaro + new Transition (8427, 8428), // &tcar -> &tcaro + new Transition (8445, 8446), // &td -> &tdo + new Transition (8461, 8540), // &th -> &tho + new Transition (8471, 8472), // &Theref -> &Therefo + new Transition (8476, 8477), // &theref -> &therefo + new Transition (8499, 8500), // &thickappr -> &thickappro + new Transition (8596, 8597), // &topb -> &topbo + new Transition (8608, 8610), // &topf -> &topfo + new Transition (8640, 8641), // &triangled -> &triangledo + new Transition (8664, 8665), // &trid -> &trido + new Transition (8681, 8682), // &TripleD -> &TripleDo + new Transition (8728, 8729), // &Tstr -> &Tstro + new Transition (8733, 8734), // &tstr -> &tstro + new Transition (8737, 8742), // &tw -> &two + new Transition (8753, 8754), // &twoheadleftarr -> &twoheadleftarro + new Transition (8764, 8765), // &twoheadrightarr -> &twoheadrightarro + new Transition (8768, 8954), // &U -> &Uo + new Transition (8775, 8959), // &u -> &uo + new Transition (8783, 8792), // &Uarr -> &Uarro + new Transition (8888, 8889), // &ulc -> &ulco + new Transition (8896, 8897), // &ulcr -> &ulcro + new Transition (8945, 8946), // &Uni -> &Unio + new Transition (8955, 8956), // &Uog -> &Uogo + new Transition (8960, 8961), // &uog -> &uogo + new Transition (8973, 8974), // &UpArr -> &UpArro + new Transition (8979, 8980), // &Uparr -> &Uparro + new Transition (8986, 8987), // &uparr -> &uparro + new Transition (8994, 8995), // &UpArrowD -> &UpArrowDo + new Transition (9000, 9001), // &UpArrowDownArr -> &UpArrowDownArro + new Transition (9004, 9005), // &UpD -> &UpDo + new Transition (9010, 9011), // &UpDownArr -> &UpDownArro + new Transition (9014, 9015), // &Upd -> &Updo + new Transition (9020, 9021), // &Updownarr -> &Updownarro + new Transition (9024, 9025), // &upd -> &updo + new Transition (9030, 9031), // &updownarr -> &updownarro + new Transition (9049, 9050), // &upharp -> &upharpo + new Transition (9050, 9051), // &upharpo -> &upharpoo + new Transition (9077, 9078), // &UpperLeftArr -> &UpperLeftArro + new Transition (9088, 9089), // &UpperRightArr -> &UpperRightArro + new Transition (9100, 9101), // &Upsil -> &Upsilo + new Transition (9104, 9105), // &upsil -> &upsilo + new Transition (9114, 9115), // &UpTeeArr -> &UpTeeArro + new Transition (9122, 9123), // &upuparr -> &upuparro + new Transition (9128, 9129), // &urc -> &urco + new Transition (9136, 9137), // &urcr -> &urcro + new Transition (9162, 9163), // &utd -> &utdo + new Transition (9201, 9436), // &v -> &vo + new Transition (9213, 9214), // &varepsil -> &varepsilo + new Transition (9223, 9224), // &varn -> &varno + new Transition (9237, 9238), // &varpr -> &varpro + new Transition (9240, 9241), // &varpropt -> &varpropto + new Transition (9249, 9250), // &varrh -> &varrho + new Transition (9303, 9432), // &V -> &Vo + new Transition (9393, 9394), // &VerticalSeparat -> &VerticalSeparato + new Transition (9441, 9442), // &vpr -> &vpro + new Transition (9484, 9523), // &W -> &Wo + new Transition (9490, 9527), // &w -> &wo + new Transition (9548, 9602), // &x -> &xo + new Transition (9565, 9607), // &X -> &Xo + new Transition (9603, 9604), // &xod -> &xodo + new Transition (9665, 9716), // &Y -> &Yo + new Transition (9672, 9720), // &y -> &yo + new Transition (9747, 9832), // &Z -> &Zo + new Transition (9754, 9836), // &z -> &zo + new Transition (9763, 9764), // &Zcar -> &Zcaro + new Transition (9769, 9770), // &zcar -> &zcaro + new Transition (9777, 9778), // &Zd -> &Zdo + new Transition (9781, 9782), // &zd -> &zdo + new Transition (9792, 9793) // &Zer -> &Zero + }; + TransitionTable_p = new Transition[278] { + new Transition (0, 6463), // & -> &p + new Transition (1, 216), // &A -> &Ap + new Transition (8, 199), // &a -> &ap + new Transition (79, 94), // &al -> &alp + new Transition (80, 86), // &ale -> &alep + new Transition (89, 90), // &Al -> &Alp + new Transition (103, 114), // &am -> & + new Transition (130, 131), // &andslo -> &andslop + new Transition (172, 173), // &angs -> &angsp + new Transition (183, 193), // &Ao -> &Aop + new Transition (188, 196), // &ao -> &aop + new Transition (199, 229), // &ap -> &app + new Transition (216, 217), // &Ap -> &App + new Transition (263, 264), // &asym -> &asymp + new Transition (301, 719), // &b -> &bp + new Transition (304, 318), // &back -> &backp + new Transition (310, 311), // &backe -> &backep + new Transition (384, 405), // &be -> &bep + new Transition (399, 400), // &bem -> &bemp + new Transition (445, 446), // &bigca -> &bigcap + new Transition (452, 453), // &bigcu -> &bigcup + new Transition (455, 460), // &bigo -> &bigop + new Transition (474, 475), // &bigsqcu -> &bigsqcup + new Transition (494, 495), // &bigtriangleu -> &bigtriangleup + new Transition (497, 498), // &bigu -> &bigup + new Transition (594, 595), // &Bo -> &Bop + new Transition (598, 599), // &bo -> &bop + new Transition (613, 662), // &box -> &boxp + new Transition (774, 775), // &bum -> &bump + new Transition (782, 783), // &Bum -> &Bump + new Transition (790, 803), // &Ca -> &Cap + new Transition (797, 805), // &ca -> &cap + new Transition (814, 815), // &capbrcu -> &capbrcup + new Transition (818, 819), // &capca -> &capcap + new Transition (821, 822), // &capcu -> &capcup + new Transition (862, 863), // &cca -> &ccap + new Transition (900, 901), // &ccu -> &ccup + new Transition (927, 928), // &cem -> &cemp + new Transition (1126, 1200), // &Co -> &Cop + new Transition (1131, 1203), // &co -> &cop + new Transition (1142, 1148), // &com -> &comp + new Transition (1278, 1283), // &csu -> &csup + new Transition (1292, 1318), // &cu -> &cup + new Transition (1301, 1302), // &cue -> &cuep + new Transition (1311, 1313), // &cularr -> &cularrp + new Transition (1315, 1316), // &Cu -> &Cup + new Transition (1323, 1324), // &cupbrca -> &cupbrcap + new Transition (1327, 1328), // &CupCa -> &CupCap + new Transition (1331, 1332), // &cupca -> &cupcap + new Transition (1334, 1335), // &cupcu -> &cupcup + new Transition (1356, 1357), // &curlyeq -> &curlyeqp + new Transition (1529, 1530), // &dem -> &demp + new Transition (1676, 1677), // &dlcro -> &dlcrop + new Transition (1679, 1689), // &do -> &dop + new Transition (1685, 1686), // &Do -> &Dop + new Transition (1694, 1719), // &dot -> &dotp + new Transition (1851, 1852), // &DoubleU -> &DoubleUp + new Transition (1907, 1908), // &DownArrowU -> &DownArrowUp + new Transition (1934, 1935), // &downhar -> &downharp + new Transition (2037, 2038), // &drcro -> &drcrop + new Transition (2108, 2326), // &E -> &Ep + new Transition (2115, 2312), // &e -> &ep + new Transition (2228, 2246), // &Em -> &Emp + new Transition (2233, 2238), // &em -> &emp + new Transition (2279, 2280), // &ems -> &emsp + new Transition (2293, 2294), // &ens -> &ensp + new Transition (2296, 2306), // &Eo -> &Eop + new Transition (2301, 2309), // &eo -> &eop + new Transition (2402, 2403), // &eqv -> &eqvp + new Transition (2458, 2472), // &ex -> &exp + new Transition (2466, 2482), // &Ex -> &Exp + new Transition (2503, 2639), // &f -> &fp + new Transition (2608, 2609), // &Fo -> &Fop + new Transition (2612, 2613), // &fo -> &fop + new Transition (2702, 2722), // &ga -> &gap + new Transition (2833, 2834), // &gna -> &gnap + new Transition (2834, 2836), // &gnap -> &gnapp + new Transition (2853, 2854), // &Go -> &Gop + new Transition (2857, 2858), // &go -> &gop + new Transition (2966, 2967), // >ra -> >rap + new Transition (2967, 2968), // >rap -> >rapp + new Transition (3024, 3025), // &hairs -> &hairsp + new Transition (3086, 3087), // &helli -> &hellip + new Transition (3106, 3107), // &HilbertS -> &HilbertSp + new Transition (3126, 3163), // &ho -> &hop + new Transition (3159, 3160), // &Ho -> &Hop + new Transition (3208, 3209), // &Hum -> &Hump + new Transition (3216, 3217), // &HumpDownHum -> &HumpDownHump + new Transition (3225, 3231), // &hy -> &hyp + new Transition (3243, 3492), // &i -> &ip + new Transition (3330, 3372), // &Im -> &Imp + new Transition (3336, 3368), // &im -> &imp + new Transition (3341, 3357), // &imag -> &imagp + new Transition (3401, 3439), // &int -> &intp + new Transition (3467, 3483), // &io -> &iop + new Transition (3471, 3480), // &Io -> &Iop + new Transition (3582, 3583), // &Jo -> &Jop + new Transition (3586, 3587), // &jo -> &jop + new Transition (3619, 3620), // &Ka -> &Kap + new Transition (3620, 3621), // &Kap -> &Kapp + new Transition (3625, 3626), // &ka -> &kap + new Transition (3626, 3627), // &kap -> &kapp + new Transition (3676, 3677), // &Ko -> &Kop + new Transition (3680, 3681), // &ko -> &kop + new Transition (3692, 4621), // &l -> &lp + new Transition (3699, 3746), // &La -> &Lap + new Transition (3705, 3744), // &la -> &lap + new Transition (3712, 3713), // &laem -> &laemp + new Transition (3766, 3782), // &larr -> &larrp + new Transition (3779, 3780), // &larrl -> &larrlp + new Transition (4006, 4007), // &lefthar -> &leftharp + new Transition (4016, 4017), // &leftharpoonu -> &leftharpoonup + new Transition (4067, 4068), // &leftrighthar -> &leftrightharp + new Transition (4138, 4139), // &LeftU -> &LeftUp + new Transition (4216, 4217), // &lessa -> &lessap + new Transition (4217, 4218), // &lessap -> &lessapp + new Transition (4402, 4403), // &lna -> &lnap + new Transition (4403, 4405), // &lnap -> &lnapp + new Transition (4422, 4560), // &lo -> &lop + new Transition (4434, 4564), // &Lo -> &Lop + new Transition (4503, 4504), // &longma -> &longmap + new Transition (4542, 4543), // &loo -> &loop + new Transition (4767, 4935), // &m -> &mp + new Transition (4768, 4785), // &ma -> &map + new Transition (4782, 4783), // &Ma -> &Map + new Transition (4801, 4802), // &mapstou -> &mapstoup + new Transition (4848, 4849), // &MediumS -> &MediumSp + new Transition (4910, 4911), // &mlc -> &mlcp + new Transition (4916, 4917), // &mn -> &mnp + new Transition (4922, 4932), // &mo -> &mop + new Transition (4928, 4929), // &Mo -> &Mop + new Transition (4945, 4946), // &mst -> &mstp + new Transition (4958, 4959), // &multima -> &multimap + new Transition (4962, 4963), // &muma -> &mumap + new Transition (4965, 5821), // &n -> &np + new Transition (4966, 4986), // &na -> &nap + new Transition (4986, 4996), // &nap -> &napp + new Transition (5011, 5012), // &nbs ->   + new Transition (5015, 5016), // &nbum -> &nbump + new Transition (5021, 5022), // &nca -> &ncap + new Transition (5052, 5053), // &ncu -> &ncup + new Transition (5097, 5098), // &NegativeMediumS -> &NegativeMediumSp + new Transition (5108, 5109), // &NegativeThickS -> &NegativeThickSp + new Transition (5115, 5116), // &NegativeThinS -> &NegativeThinSp + new Transition (5129, 5130), // &NegativeVeryThinS -> &NegativeVeryThinSp + new Transition (5227, 5236), // &nh -> &nhp + new Transition (5347, 5369), // &No -> &Nop + new Transition (5363, 5364), // &NonBreakingS -> &NonBreakingSp + new Transition (5372, 5373), // &no -> &nop + new Transition (5390, 5391), // &NotCu -> &NotCup + new Transition (5393, 5394), // &NotCupCa -> &NotCupCap + new Transition (5495, 5496), // &NotHum -> &NotHump + new Transition (5503, 5504), // &NotHumpDownHum -> &NotHumpDownHump + new Transition (5701, 5713), // &NotSquareSu -> &NotSquareSup + new Transition (5726, 5768), // &NotSu -> &NotSup + new Transition (5895, 5938), // &ns -> &nsp + new Transition (5913, 5918), // &nshort -> &nshortp + new Transition (5944, 5948), // &nsqsu -> &nsqsup + new Transition (5951, 5973), // &nsu -> &nsup + new Transition (6040, 6041), // &nums -> &numsp + new Transition (6044, 6045), // &nva -> &nvap + new Transition (6131, 6306), // &O -> &Op + new Transition (6138, 6302), // &o -> &op + new Transition (6294, 6295), // &Oo -> &Oop + new Transition (6298, 6299), // &oo -> &oop + new Transition (6333, 6334), // &oper -> &operp + new Transition (6370, 6371), // &orslo -> &orslop + new Transition (6498, 6511), // &per -> &perp + new Transition (6609, 6630), // &Po -> &Pop + new Transition (6615, 6616), // &Poincare -> &Poincarep + new Transition (6622, 6633), // &po -> &pop + new Transition (6644, 6645), // &pra -> &prap + new Transition (6657, 6658), // &preca -> &precap + new Transition (6658, 6659), // &precap -> &precapp + new Transition (6706, 6707), // &precna -> &precnap + new Transition (6707, 6708), // &precnap -> &precnapp + new Transition (6736, 6737), // &prna -> &prnap + new Transition (6745, 6770), // &pro -> &prop + new Transition (6748, 6772), // &Pro -> &Prop + new Transition (6810, 6811), // &puncs -> &puncsp + new Transition (6817, 6833), // &q -> &qp + new Transition (6825, 6826), // &Qo -> &Qop + new Transition (6829, 6830), // &qo -> &qop + new Transition (6876, 7512), // &r -> &rp + new Transition (6902, 6903), // &raem -> &raemp + new Transition (6932, 6953), // &rarr -> &rarrp + new Transition (6934, 6935), // &rarra -> &rarrap + new Transition (6950, 6951), // &rarrl -> &rarrlp + new Transition (7076, 7082), // &real -> &realp + new Transition (7121, 7122), // &ReverseU -> &ReverseUp + new Transition (7281, 7282), // &righthar -> &rightharp + new Transition (7291, 7292), // &rightharpoonu -> &rightharpoonup + new Transition (7307, 7308), // &rightlefthar -> &rightleftharp + new Transition (7383, 7384), // &RightU -> &RightUp + new Transition (7469, 7481), // &ro -> &rop + new Transition (7485, 7486), // &Ro -> &Rop + new Transition (7505, 7506), // &RoundIm -> &RoundImp + new Transition (7512, 7519), // &rp -> &rpp + new Transition (7617, 7956), // &s -> &sp + new Transition (7631, 7680), // &sc -> &scp + new Transition (7633, 7634), // &sca -> &scap + new Transition (7671, 7672), // &scna -> &scnap + new Transition (7753, 7754), // &shar -> &sharp + new Transition (7798, 7803), // &short -> &shortp + new Transition (7823, 7824), // &ShortU -> &ShortUp + new Transition (7847, 7868), // &sim -> &simp + new Transition (7908, 7909), // &smash -> &smashp + new Transition (7911, 7912), // &sme -> &smep + new Transition (7936, 7953), // &so -> &sop + new Transition (7949, 7950), // &So -> &Sop + new Transition (7970, 7971), // &sqca -> &sqcap + new Transition (7975, 7976), // &sqcu -> &sqcup + new Transition (7985, 7997), // &sqsu -> &sqsup + new Transition (8033, 8045), // &SquareSu -> &SquareSup + new Transition (8111, 8120), // &straight -> &straightp + new Transition (8112, 8113), // &straighte -> &straightep + new Transition (8127, 8282), // &Su -> &Sup + new Transition (8130, 8284), // &su -> &sup + new Transition (8131, 8155), // &sub -> &subp + new Transition (8193, 8196), // &subsu -> &subsup + new Transition (8201, 8202), // &succa -> &succap + new Transition (8202, 8203), // &succap -> &succapp + new Transition (8250, 8251), // &succna -> &succnap + new Transition (8251, 8252), // &succnap -> &succnapp + new Transition (8284, 8343), // &sup -> &supp + new Transition (8370, 8373), // &supsu -> &supsup + new Transition (8404, 8617), // &t -> &tp + new Transition (8496, 8497), // &thicka -> &thickap + new Transition (8497, 8498), // &thickap -> &thickapp + new Transition (8510, 8511), // &ThickS -> &ThickSp + new Transition (8517, 8518), // &thins -> &thinsp + new Transition (8521, 8522), // &ThinS -> &ThinSp + new Transition (8528, 8529), // &thka -> &thkap + new Transition (8590, 8594), // &to -> &top + new Transition (8604, 8605), // &To -> &Top + new Transition (8628, 8698), // &tr -> &trp + new Transition (8633, 8685), // &tri -> &trip + new Transition (8677, 8678), // &Tri -> &Trip + new Transition (8768, 8970), // &U -> &Up + new Transition (8775, 8983), // &u -> &up + new Transition (8897, 8898), // &ulcro -> &ulcrop + new Transition (8954, 8964), // &Uo -> &Uop + new Transition (8959, 8967), // &uo -> &uop + new Transition (8970, 9068), // &Up -> &Upp + new Transition (9048, 9049), // &uphar -> &upharp + new Transition (9118, 9119), // &upu -> &upup + new Transition (9137, 9138), // &urcro -> &urcrop + new Transition (9201, 9440), // &v -> &vp + new Transition (9208, 9231), // &var -> &varp + new Transition (9209, 9210), // &vare -> &varep + new Transition (9218, 9219), // &varka -> &varkap + new Transition (9219, 9220), // &varkap -> &varkapp + new Transition (9238, 9239), // &varpro -> &varprop + new Transition (9258, 9269), // &varsu -> &varsup + new Transition (9357, 9358), // &velli -> &vellip + new Transition (9388, 9389), // &VerticalSe -> &VerticalSep + new Transition (9408, 9409), // &VeryThinS -> &VeryThinSp + new Transition (9427, 9430), // &vnsu -> &vnsup + new Transition (9432, 9433), // &Vo -> &Vop + new Transition (9436, 9437), // &vo -> &vop + new Transition (9442, 9443), // &vpro -> &vprop + new Transition (9458, 9465), // &vsu -> &vsup + new Transition (9490, 9531), // &w -> &wp + new Transition (9514, 9515), // &weier -> &weierp + new Transition (9523, 9524), // &Wo -> &Wop + new Transition (9527, 9528), // &wo -> &wop + new Transition (9550, 9551), // &xca -> &xcap + new Transition (9557, 9558), // &xcu -> &xcup + new Transition (9595, 9596), // &xma -> &xmap + new Transition (9602, 9611), // &xo -> &xop + new Transition (9607, 9608), // &Xo -> &Xop + new Transition (9642, 9643), // &xsqcu -> &xsqcup + new Transition (9645, 9646), // &xu -> &xup + new Transition (9716, 9717), // &Yo -> &Yop + new Transition (9720, 9721), // &yo -> &yop + new Transition (9799, 9800), // &ZeroWidthS -> &ZeroWidthSp + new Transition (9832, 9833), // &Zo -> &Zop + new Transition (9836, 9837) // &zo -> &zop + }; + TransitionTable_q = new Transition[144] { + new Transition (0, 6817), // & -> &q + new Transition (234, 235), // &approxe -> &approxeq + new Transition (266, 267), // &asympe -> &asympeq + new Transition (328, 329), // &backsime -> &backsimeq + new Transition (379, 380), // &bd -> &bdq + new Transition (471, 472), // &bigs -> &bigsq + new Transition (531, 532), // &blacks -> &blacksq + new Transition (580, 582), // &bne -> &bneq + new Transition (779, 787), // &bumpe -> &bumpeq + new Transition (784, 785), // &Bumpe -> &Bumpeq + new Transition (983, 984), // &circe -> &circeq + new Transition (1138, 1140), // &colone -> &coloneq + new Transition (1355, 1356), // &curlye -> &curlyeq + new Transition (1513, 1514), // &ddotse -> &ddotseq + new Transition (1700, 1701), // &dote -> &doteq + new Transition (1707, 1708), // &DotE -> &DotEq + new Transition (1724, 1725), // &dots -> &dotsq + new Transition (2108, 2367), // &E -> &Eq + new Transition (2115, 2339), // &e -> &eq + new Transition (2254, 2255), // &EmptySmallS -> &EmptySmallSq + new Transition (2272, 2273), // &EmptyVerySmallS -> &EmptyVerySmallSq + new Transition (2514, 2515), // &fallingdotse -> &fallingdotseq + new Transition (2564, 2565), // &FilledSmallS -> &FilledSmallSq + new Transition (2580, 2581), // &FilledVerySmallS -> &FilledVerySmallSq + new Transition (2765, 2771), // &ge -> &geq + new Transition (2771, 2773), // &geq -> &geqq + new Transition (2843, 2845), // &gne -> &gneq + new Transition (2845, 2847), // &gneq -> &gneqq + new Transition (2872, 2873), // &GreaterE -> &GreaterEq + new Transition (2887, 2888), // &GreaterFullE -> &GreaterFullEq + new Transition (2911, 2912), // &GreaterSlantE -> &GreaterSlantEq + new Transition (2942, 2959), // > -> >q + new Transition (2980, 2981), // >re -> >req + new Transition (2981, 2987), // >req -> >reqq + new Transition (3007, 3008), // &gvertne -> &gvertneq + new Transition (3008, 3009), // &gvertneq -> &gvertneqq + new Transition (3219, 3220), // &HumpE -> &HumpEq + new Transition (3243, 3497), // &i -> &iq + new Transition (3705, 3755), // &la -> &laq + new Transition (3869, 3873), // &ld -> &ldq + new Transition (3896, 4187), // &le -> &leq + new Transition (4074, 4075), // &leftrights -> &leftrightsq + new Transition (4132, 4133), // &LeftTriangleE -> &LeftTriangleEq + new Transition (4187, 4189), // &leq -> &leqq + new Transition (4227, 4228), // &lesse -> &lesseq + new Transition (4228, 4233), // &lesseq -> &lesseqq + new Transition (4240, 4241), // &LessE -> &LessEq + new Transition (4257, 4258), // &LessFullE -> &LessFullEq + new Transition (4289, 4290), // &LessSlantE -> &LessSlantEq + new Transition (4412, 4414), // &lne -> &lneq + new Transition (4414, 4416), // &lneq -> &lneqq + new Transition (4652, 4676), // &ls -> &lsq + new Transition (4653, 4654), // &lsa -> &lsaq + new Transition (4698, 4725), // < -> <q + new Transition (4760, 4761), // &lvertne -> &lvertneq + new Transition (4761, 4762), // &lvertneq -> &lvertneqq + new Transition (5064, 5135), // &ne -> &neq + new Transition (5198, 5200), // &nge -> &ngeq + new Transition (5200, 5202), // &ngeq -> &ngeqq + new Transition (5270, 5312), // &nle -> &nleq + new Transition (5312, 5314), // &nleq -> &nleqq + new Transition (5414, 5422), // &NotE -> &NotEq + new Transition (5447, 5448), // &NotGreaterE -> &NotGreaterEq + new Transition (5457, 5458), // &NotGreaterFullE -> &NotGreaterFullEq + new Transition (5481, 5482), // &NotGreaterSlantE -> &NotGreaterSlantEq + new Transition (5506, 5507), // &NotHumpE -> &NotHumpEq + new Transition (5545, 5546), // &NotLeftTriangleE -> &NotLeftTriangleEq + new Transition (5554, 5555), // &NotLessE -> &NotLessEq + new Transition (5578, 5579), // &NotLessSlantE -> &NotLessSlantEq + new Transition (5639, 5640), // &NotPrecedesE -> &NotPrecedesEq + new Transition (5650, 5651), // &NotPrecedesSlantE -> &NotPrecedesSlantEq + new Transition (5688, 5689), // &NotRightTriangleE -> &NotRightTriangleEq + new Transition (5694, 5695), // &NotS -> &NotSq + new Transition (5707, 5708), // &NotSquareSubsetE -> &NotSquareSubsetEq + new Transition (5720, 5721), // &NotSquareSupersetE -> &NotSquareSupersetEq + new Transition (5732, 5733), // &NotSubsetE -> &NotSubsetEq + new Transition (5745, 5746), // &NotSucceedsE -> &NotSucceedsEq + new Transition (5756, 5757), // &NotSucceedsSlantE -> &NotSucceedsSlantEq + new Transition (5775, 5776), // &NotSupersetE -> &NotSupersetEq + new Transition (5787, 5788), // &NotTildeE -> &NotTildeEq + new Transition (5797, 5798), // &NotTildeFullE -> &NotTildeFullEq + new Transition (5852, 5853), // &nprece -> &npreceq + new Transition (5895, 5942), // &ns -> &nsq + new Transition (5930, 5932), // &nsime -> &nsimeq + new Transition (5962, 5963), // &nsubsete -> &nsubseteq + new Transition (5963, 5965), // &nsubseteq -> &nsubseteqq + new Transition (5970, 5971), // &nsucce -> &nsucceq + new Transition (5983, 5984), // &nsupsete -> &nsupseteq + new Transition (5984, 5986), // &nsupseteq -> &nsupseteqq + new Transition (6018, 6019), // &ntrianglelefte -> &ntrianglelefteq + new Transition (6027, 6028), // &ntrianglerighte -> &ntrianglerighteq + new Transition (6669, 6670), // &preccurlye -> &preccurlyeq + new Transition (6679, 6680), // &PrecedesE -> &PrecedesEq + new Transition (6690, 6691), // &PrecedesSlantE -> &PrecedesSlantEq + new Transition (6702, 6703), // &prece -> &preceq + new Transition (6713, 6714), // &precne -> &precneq + new Transition (6714, 6715), // &precneq -> &precneqq + new Transition (6866, 6867), // &queste -> &questeq + new Transition (6882, 6921), // &ra -> &raq + new Transition (7053, 7063), // &rd -> &rdq + new Transition (7102, 7110), // &ReverseE -> &ReverseEq + new Transition (7123, 7124), // &ReverseUpE -> &ReverseUpEq + new Transition (7326, 7327), // &rights -> &rightsq + new Transition (7377, 7378), // &RightTriangleE -> &RightTriangleEq + new Transition (7439, 7440), // &risingdotse -> &risingdotseq + new Transition (7542, 7559), // &rs -> &rsq + new Transition (7543, 7544), // &rsa -> &rsaq + new Transition (7610, 7980), // &S -> &Sq + new Transition (7617, 7968), // &s -> &sq + new Transition (7624, 7625), // &sb -> &sbq + new Transition (7853, 7855), // &sime -> &simeq + new Transition (7994, 7995), // &sqsubsete -> &sqsubseteq + new Transition (8005, 8006), // &sqsupsete -> &sqsupseteq + new Transition (8039, 8040), // &SquareSubsetE -> &SquareSubsetEq + new Transition (8052, 8053), // &SquareSupersetE -> &SquareSupersetEq + new Transition (8173, 8174), // &subsete -> &subseteq + new Transition (8174, 8176), // &subseteq -> &subseteqq + new Transition (8178, 8179), // &SubsetE -> &SubsetEq + new Transition (8185, 8186), // &subsetne -> &subsetneq + new Transition (8186, 8188), // &subsetneq -> &subsetneqq + new Transition (8213, 8214), // &succcurlye -> &succcurlyeq + new Transition (8223, 8224), // &SucceedsE -> &SucceedsEq + new Transition (8234, 8235), // &SucceedsSlantE -> &SucceedsSlantEq + new Transition (8246, 8247), // &succe -> &succeq + new Transition (8257, 8258), // &succne -> &succneq + new Transition (8258, 8259), // &succneq -> &succneqq + new Transition (8314, 8315), // &SupersetE -> &SupersetEq + new Transition (8356, 8357), // &supsete -> &supseteq + new Transition (8357, 8359), // &supseteq -> &supseteqq + new Transition (8362, 8363), // &supsetne -> &supsetneq + new Transition (8363, 8365), // &supsetneq -> &supsetneqq + new Transition (8554, 8555), // &TildeE -> &TildeEq + new Transition (8564, 8565), // &TildeFullE -> &TildeFullEq + new Transition (8638, 8653), // &triangle -> &triangleq + new Transition (8650, 8651), // &trianglelefte -> &trianglelefteq + new Transition (8661, 8662), // &trianglerighte -> &trianglerighteq + new Transition (9034, 9035), // &UpE -> &UpEq + new Transition (9264, 9265), // &varsubsetne -> &varsubsetneq + new Transition (9265, 9267), // &varsubsetneq -> &varsubsetneqq + new Transition (9274, 9275), // &varsupsetne -> &varsupsetneq + new Transition (9275, 9277), // &varsupsetneq -> &varsupsetneqq + new Transition (9352, 9353), // &veee -> &veeeq + new Transition (9508, 9510), // &wedge -> &wedgeq + new Transition (9636, 9640) // &xs -> &xsq + }; + TransitionTable_r = new Transition[942] { + new Transition (0, 6876), // & -> &r + new Transition (1, 237), // &A -> &Ar + new Transition (8, 242), // &a -> &ar + new Transition (15, 16), // &Ab -> &Abr + new Transition (21, 22), // &ab -> &abr + new Transition (34, 35), // &Aci -> &Acir + new Transition (38, 39), // &aci -> &acir + new Transition (60, 65), // &af -> &afr + new Transition (62, 63), // &Af -> &Afr + new Transition (67, 68), // &Ag -> &Agr + new Transition (73, 74), // &ag -> &agr + new Transition (100, 101), // &Amac -> &Amacr + new Transition (105, 106), // &amac -> &amacr + new Transition (136, 164), // &ang -> &angr + new Transition (179, 180), // &angza -> &angzar + new Transition (180, 181), // &angzar -> &angzarr + new Transition (203, 204), // &apaci -> &apacir + new Transition (229, 230), // &app -> &appr + new Transition (248, 249), // &Asc -> &Ascr + new Transition (252, 253), // &asc -> &ascr + new Transition (301, 730), // &b -> &br + new Transition (302, 344), // &ba -> &bar + new Transition (318, 319), // &backp -> &backpr + new Transition (331, 725), // &B -> &Br + new Transition (332, 341), // &Ba -> &Bar + new Transition (360, 361), // &bb -> &bbr + new Transition (365, 366), // &bbrktb -> &bbrktbr + new Transition (384, 409), // &be -> &ber + new Transition (390, 414), // &Be -> &Ber + new Transition (436, 437), // &Bf -> &Bfr + new Transition (439, 440), // &bf -> &bfr + new Transition (448, 449), // &bigci -> &bigcir + new Transition (478, 479), // &bigsta -> &bigstar + new Transition (481, 482), // &bigt -> &bigtr + new Transition (514, 515), // &bka -> &bkar + new Transition (534, 535), // &blacksqua -> &blacksquar + new Transition (538, 539), // &blackt -> &blacktr + new Transition (545, 557), // &blacktriangle -> &blacktriangler + new Transition (618, 630), // &boxD -> &boxDr + new Transition (623, 634), // &boxd -> &boxdr + new Transition (673, 685), // &boxU -> &boxUr + new Transition (678, 689), // &boxu -> &boxur + new Transition (691, 713), // &boxV -> &boxVr + new Transition (693, 717), // &boxv -> &boxvr + new Transition (719, 720), // &bp -> &bpr + new Transition (737, 738), // &brvba -> ¦ + new Transition (741, 742), // &Bsc -> &Bscr + new Transition (745, 746), // &bsc -> &bscr + new Transition (789, 1261), // &C -> &Cr + new Transition (796, 1256), // &c -> &cr + new Transition (797, 848), // &ca -> &car + new Transition (811, 812), // &capb -> &capbr + new Transition (836, 837), // &CapitalDiffe -> &CapitalDiffer + new Transition (862, 872), // &cca -> &ccar + new Transition (867, 868), // &Cca -> &Ccar + new Transition (886, 887), // &Cci -> &Ccir + new Transition (890, 891), // &cci -> &ccir + new Transition (938, 939), // &Cente -> &Center + new Transition (944, 945), // ¢e -> ¢er + new Transition (950, 951), // &Cf -> &Cfr + new Transition (953, 954), // &cf -> &cfr + new Transition (969, 970), // &checkma -> &checkmar + new Transition (978, 979), // &ci -> &cir + new Transition (988, 989), // &circlea -> &circlear + new Transition (989, 990), // &circlear -> &circlearr + new Transition (992, 998), // &circlearrow -> &circlearrowr + new Transition (1010, 1011), // &circledci -> &circledcir + new Transition (1019, 1020), // &Ci -> &Cir + new Transition (1065, 1066), // &cirsci -> &cirscir + new Transition (1081, 1082), // &ClockwiseContou -> &ClockwiseContour + new Transition (1087, 1088), // &ClockwiseContourInteg -> &ClockwiseContourIntegr + new Transition (1095, 1096), // &CloseCu -> &CloseCur + new Transition (1172, 1173), // &Cong -> &Congr + new Transition (1189, 1190), // &Contou -> &Contour + new Transition (1195, 1196), // &ContourInteg -> &ContourIntegr + new Transition (1200, 1210), // &Cop -> &Copr + new Transition (1203, 1206), // &cop -> &copr + new Transition (1223, 1224), // ©s -> ©sr + new Transition (1229, 1230), // &Counte -> &Counter + new Transition (1245, 1246), // &CounterClockwiseContou -> &CounterClockwiseContour + new Transition (1251, 1252), // &CounterClockwiseContourInteg -> &CounterClockwiseContourIntegr + new Transition (1257, 1258), // &cra -> &crar + new Transition (1258, 1259), // &crar -> &crarr + new Transition (1271, 1272), // &Csc -> &Cscr + new Transition (1275, 1276), // &csc -> &cscr + new Transition (1292, 1346), // &cu -> &cur + new Transition (1294, 1295), // &cuda -> &cudar + new Transition (1295, 1296), // &cudar -> &cudarr + new Transition (1296, 1299), // &cudarr -> &cudarrr + new Transition (1302, 1303), // &cuep -> &cuepr + new Transition (1309, 1310), // &cula -> &cular + new Transition (1310, 1311), // &cular -> &cularr + new Transition (1320, 1321), // &cupb -> &cupbr + new Transition (1341, 1342), // &cupo -> &cupor + new Transition (1346, 1377), // &cur -> &curr + new Transition (1347, 1348), // &cura -> &curar + new Transition (1348, 1349), // &curar -> &curarr + new Transition (1357, 1358), // &curlyeqp -> &curlyeqpr + new Transition (1383, 1384), // &curvea -> &curvear + new Transition (1384, 1385), // &curvear -> &curvearr + new Transition (1387, 1393), // &curvearrow -> &curvearrowr + new Transition (1426, 1444), // &Da -> &Dar + new Transition (1429, 1430), // &Dagge -> &Dagger + new Transition (1432, 2023), // &d -> &dr + new Transition (1433, 1451), // &da -> &dar + new Transition (1436, 1437), // &dagge -> &dagger + new Transition (1444, 1445), // &Dar -> &Darr + new Transition (1447, 1448), // &dA -> &dAr + new Transition (1448, 1449), // &dAr -> &dArr + new Transition (1451, 1452), // &dar -> &darr + new Transition (1465, 1466), // &dbka -> &dbkar + new Transition (1475, 1476), // &Dca -> &Dcar + new Transition (1481, 1482), // &dca -> &dcar + new Transition (1494, 1500), // &dda -> &ddar + new Transition (1497, 1498), // &ddagge -> &ddagger + new Transition (1500, 1501), // &ddar -> &ddarr + new Transition (1504, 1505), // &DDot -> &DDotr + new Transition (1535, 1544), // &df -> &dfr + new Transition (1541, 1542), // &Df -> &Dfr + new Transition (1547, 1548), // &dHa -> &dHar + new Transition (1551, 1552), // &dha -> &dhar + new Transition (1552, 1555), // &dhar -> &dharr + new Transition (1559, 1560), // &Diac -> &Diacr + new Transition (1587, 1588), // &DiacriticalG -> &DiacriticalGr + new Transition (1623, 1624), // &Diffe -> &Differ + new Transition (1670, 1675), // &dlc -> &dlcr + new Transition (1671, 1672), // &dlco -> &dlcor + new Transition (1682, 1683), // &dolla -> &dollar + new Transition (1727, 1728), // &dotsqua -> &dotsquar + new Transition (1736, 1737), // &doubleba -> &doublebar + new Transition (1753, 1754), // &DoubleContou -> &DoubleContour + new Transition (1759, 1760), // &DoubleContourInteg -> &DoubleContourIntegr + new Transition (1770, 1771), // &DoubleDownA -> &DoubleDownAr + new Transition (1771, 1772), // &DoubleDownAr -> &DoubleDownArr + new Transition (1780, 1781), // &DoubleLeftA -> &DoubleLeftAr + new Transition (1781, 1782), // &DoubleLeftAr -> &DoubleLeftArr + new Transition (1791, 1792), // &DoubleLeftRightA -> &DoubleLeftRightAr + new Transition (1792, 1793), // &DoubleLeftRightAr -> &DoubleLeftRightArr + new Transition (1808, 1809), // &DoubleLongLeftA -> &DoubleLongLeftAr + new Transition (1809, 1810), // &DoubleLongLeftAr -> &DoubleLongLeftArr + new Transition (1819, 1820), // &DoubleLongLeftRightA -> &DoubleLongLeftRightAr + new Transition (1820, 1821), // &DoubleLongLeftRightAr -> &DoubleLongLeftRightArr + new Transition (1830, 1831), // &DoubleLongRightA -> &DoubleLongRightAr + new Transition (1831, 1832), // &DoubleLongRightAr -> &DoubleLongRightArr + new Transition (1841, 1842), // &DoubleRightA -> &DoubleRightAr + new Transition (1842, 1843), // &DoubleRightAr -> &DoubleRightArr + new Transition (1853, 1854), // &DoubleUpA -> &DoubleUpAr + new Transition (1854, 1855), // &DoubleUpAr -> &DoubleUpArr + new Transition (1863, 1864), // &DoubleUpDownA -> &DoubleUpDownAr + new Transition (1864, 1865), // &DoubleUpDownAr -> &DoubleUpDownArr + new Transition (1870, 1871), // &DoubleVe -> &DoubleVer + new Transition (1878, 1879), // &DoubleVerticalBa -> &DoubleVerticalBar + new Transition (1883, 1884), // &DownA -> &DownAr + new Transition (1884, 1885), // &DownAr -> &DownArr + new Transition (1889, 1890), // &Downa -> &Downar + new Transition (1890, 1891), // &Downar -> &Downarr + new Transition (1897, 1898), // &downa -> &downar + new Transition (1898, 1899), // &downar -> &downarr + new Transition (1904, 1905), // &DownArrowBa -> &DownArrowBar + new Transition (1909, 1910), // &DownArrowUpA -> &DownArrowUpAr + new Transition (1910, 1911), // &DownArrowUpAr -> &DownArrowUpArr + new Transition (1915, 1916), // &DownB -> &DownBr + new Transition (1925, 1926), // &downdowna -> &downdownar + new Transition (1926, 1927), // &downdownar -> &downdownarr + new Transition (1933, 1934), // &downha -> &downhar + new Transition (1938, 1944), // &downharpoon -> &downharpoonr + new Transition (1963, 1964), // &DownLeftRightVecto -> &DownLeftRightVector + new Transition (1973, 1974), // &DownLeftTeeVecto -> &DownLeftTeeVector + new Transition (1980, 1981), // &DownLeftVecto -> &DownLeftVector + new Transition (1984, 1985), // &DownLeftVectorBa -> &DownLeftVectorBar + new Transition (1999, 2000), // &DownRightTeeVecto -> &DownRightTeeVector + new Transition (2006, 2007), // &DownRightVecto -> &DownRightVector + new Transition (2010, 2011), // &DownRightVectorBa -> &DownRightVectorBar + new Transition (2017, 2018), // &DownTeeA -> &DownTeeAr + new Transition (2018, 2019), // &DownTeeAr -> &DownTeeArr + new Transition (2026, 2027), // &drbka -> &drbkar + new Transition (2031, 2036), // &drc -> &drcr + new Transition (2032, 2033), // &drco -> &drcor + new Transition (2041, 2042), // &Dsc -> &Dscr + new Transition (2045, 2046), // &dsc -> &dscr + new Transition (2057, 2058), // &Dst -> &Dstr + new Transition (2062, 2063), // &dst -> &dstr + new Transition (2067, 2072), // &dt -> &dtr + new Transition (2078, 2079), // &dua -> &duar + new Transition (2079, 2080), // &duar -> &duarr + new Transition (2083, 2084), // &duha -> &duhar + new Transition (2102, 2103), // &dzig -> &dzigr + new Transition (2104, 2105), // &dzigra -> &dzigrar + new Transition (2105, 2106), // &dzigrar -> &dzigrarr + new Transition (2115, 2409), // &e -> &er + new Transition (2124, 2125), // &easte -> &easter + new Transition (2128, 2129), // &Eca -> &Ecar + new Transition (2134, 2135), // &eca -> &ecar + new Transition (2139, 2140), // &eci -> &ecir + new Transition (2142, 2143), // &Eci -> &Ecir + new Transition (2175, 2183), // &ef -> &efr + new Transition (2180, 2181), // &Ef -> &Efr + new Transition (2185, 2193), // &eg -> &egr + new Transition (2187, 2188), // &Eg -> &Egr + new Transition (2216, 2217), // &elinte -> &elinter + new Transition (2230, 2231), // &Emac -> &Emacr + new Transition (2235, 2236), // &emac -> &emacr + new Transition (2257, 2258), // &EmptySmallSqua -> &EmptySmallSquar + new Transition (2264, 2265), // &EmptyVe -> &EmptyVer + new Transition (2275, 2276), // &EmptyVerySmallSqua -> &EmptyVerySmallSquar + new Transition (2313, 2314), // &epa -> &epar + new Transition (2341, 2342), // &eqci -> &eqcir + new Transition (2359, 2360), // &eqslantgt -> &eqslantgtr + new Transition (2390, 2391), // &Equilib -> &Equilibr + new Transition (2404, 2405), // &eqvpa -> &eqvpar + new Transition (2410, 2411), // &era -> &erar + new Transition (2411, 2412), // &erar -> &erarr + new Transition (2419, 2420), // &Esc -> &Escr + new Transition (2423, 2424), // &esc -> &escr + new Transition (2451, 2455), // &eu -> &eur + new Transition (2503, 2647), // &f -> &fr + new Transition (2530, 2547), // &ff -> &ffr + new Transition (2544, 2545), // &Ff -> &Ffr + new Transition (2567, 2568), // &FilledSmallSqua -> &FilledSmallSquar + new Transition (2572, 2573), // &FilledVe -> &FilledVer + new Transition (2583, 2584), // &FilledVerySmallSqua -> &FilledVerySmallSquar + new Transition (2608, 2616), // &Fo -> &For + new Transition (2612, 2621), // &fo -> &for + new Transition (2630, 2631), // &Fou -> &Four + new Transition (2633, 2634), // &Fourie -> &Fourier + new Transition (2635, 2636), // &Fouriert -> &Fouriertr + new Transition (2640, 2641), // &fpa -> &fpar + new Transition (2694, 2695), // &Fsc -> &Fscr + new Transition (2698, 2699), // &fsc -> &fscr + new Transition (2701, 2861), // &g -> &gr + new Transition (2708, 2866), // &G -> &Gr + new Transition (2724, 2725), // &Gb -> &Gbr + new Transition (2730, 2731), // &gb -> &gbr + new Transition (2742, 2743), // &Gci -> &Gcir + new Transition (2747, 2748), // &gci -> &gcir + new Transition (2799, 2800), // &Gf -> &Gfr + new Transition (2802, 2803), // &gf -> &gfr + new Transition (2836, 2837), // &gnapp -> &gnappr + new Transition (2870, 2871), // &Greate -> &Greater + new Transition (2893, 2894), // &GreaterG -> &GreaterGr + new Transition (2898, 2899), // &GreaterGreate -> &GreaterGreater + new Transition (2924, 2925), // &Gsc -> &Gscr + new Transition (2928, 2929), // &gsc -> &gscr + new Transition (2942, 2965), // > -> >r + new Transition (2947, 2948), // >ci -> >cir + new Transition (2956, 2957), // >lPa -> >lPar + new Transition (2966, 2973), // >ra -> >rar + new Transition (2968, 2969), // >rapp -> >rappr + new Transition (2973, 2974), // >rar -> >rarr + new Transition (3003, 3004), // &gve -> &gver + new Transition (3021, 3041), // &ha -> &har + new Transition (3022, 3023), // &hai -> &hair + new Transition (3041, 3050), // &har -> &harr + new Transition (3046, 3047), // &hA -> &hAr + new Transition (3047, 3048), // &hAr -> &hArr + new Transition (3053, 3054), // &harrci -> &harrcir + new Transition (3061, 3062), // &hba -> &hbar + new Transition (3065, 3066), // &Hci -> &Hcir + new Transition (3070, 3071), // &hci -> &hcir + new Transition (3074, 3089), // &he -> &her + new Transition (3075, 3076), // &hea -> &hear + new Transition (3094, 3095), // &Hf -> &Hfr + new Transition (3097, 3098), // &hf -> &hfr + new Transition (3103, 3104), // &Hilbe -> &Hilber + new Transition (3115, 3116), // &hksea -> &hksear + new Transition (3121, 3122), // &hkswa -> &hkswar + new Transition (3126, 3166), // &ho -> &hor + new Transition (3127, 3128), // &hoa -> &hoar + new Transition (3128, 3129), // &hoar -> &hoarr + new Transition (3137, 3148), // &hook -> &hookr + new Transition (3142, 3143), // &hooklefta -> &hookleftar + new Transition (3143, 3144), // &hookleftar -> &hookleftarr + new Transition (3153, 3154), // &hookrighta -> &hookrightar + new Transition (3154, 3155), // &hookrightar -> &hookrightarr + new Transition (3159, 3171), // &Ho -> &Hor + new Transition (3168, 3169), // &horba -> &horbar + new Transition (3185, 3186), // &Hsc -> &Hscr + new Transition (3189, 3190), // &hsc -> &hscr + new Transition (3197, 3198), // &Hst -> &Hstr + new Transition (3202, 3203), // &hst -> &hstr + new Transition (3253, 3254), // &Ici -> &Icir + new Transition (3257, 3258), // &ici -> &icir + new Transition (3281, 3287), // &if -> &ifr + new Transition (3284, 3285), // &If -> &Ifr + new Transition (3289, 3290), // &Ig -> &Igr + new Transition (3295, 3296), // &ig -> &igr + new Transition (3333, 3334), // &Imac -> &Imacr + new Transition (3338, 3339), // &imac -> &imacr + new Transition (3347, 3348), // &Imagina -> &Imaginar + new Transition (3358, 3359), // &imagpa -> &imagpar + new Transition (3381, 3382), // &inca -> &incar + new Transition (3407, 3419), // &inte -> &inter + new Transition (3409, 3410), // &intege -> &integer + new Transition (3413, 3424), // &Inte -> &Inter + new Transition (3414, 3415), // &Integ -> &Integr + new Transition (3434, 3435), // &intla -> &intlar + new Transition (3439, 3440), // &intp -> &intpr + new Transition (3492, 3493), // &ip -> &ipr + new Transition (3504, 3505), // &Isc -> &Iscr + new Transition (3508, 3509), // &isc -> &iscr + new Transition (3557, 3558), // &Jci -> &Jcir + new Transition (3563, 3564), // &jci -> &jcir + new Transition (3571, 3572), // &Jf -> &Jfr + new Transition (3574, 3575), // &jf -> &jfr + new Transition (3591, 3592), // &Jsc -> &Jscr + new Transition (3595, 3596), // &jsc -> &jscr + new Transition (3598, 3599), // &Jse -> &Jser + new Transition (3603, 3604), // &jse -> &jser + new Transition (3648, 3649), // &Kf -> &Kfr + new Transition (3651, 3652), // &kf -> &kfr + new Transition (3654, 3655), // &kg -> &kgr + new Transition (3685, 3686), // &Ksc -> &Kscr + new Transition (3689, 3690), // &ksc -> &kscr + new Transition (3692, 4628), // &l -> &lr + new Transition (3693, 3762), // &lA -> &lAr + new Transition (3694, 3695), // &lAa -> &lAar + new Transition (3695, 3696), // &lAar -> &lAarr + new Transition (3699, 3759), // &La -> &Lar + new Transition (3705, 3765), // &la -> &lar + new Transition (3718, 3719), // &lag -> &lagr + new Transition (3751, 3752), // &Laplacet -> &Laplacetr + new Transition (3759, 3760), // &Lar -> &Larr + new Transition (3762, 3763), // &lAr -> &lArr + new Transition (3765, 3766), // &lar -> &larr + new Transition (3808, 3809), // &lBa -> &lBar + new Transition (3809, 3810), // &lBar -> &lBarr + new Transition (3812, 3821), // &lb -> &lbr + new Transition (3813, 3814), // &lba -> &lbar + new Transition (3814, 3815), // &lbar -> &lbarr + new Transition (3817, 3818), // &lbb -> &lbbr + new Transition (3838, 3839), // &Lca -> &Lcar + new Transition (3844, 3845), // &lca -> &lcar + new Transition (3869, 3879), // &ld -> &ldr + new Transition (3875, 3877), // &ldquo -> &ldquor + new Transition (3882, 3883), // &ldrdha -> &ldrdhar + new Transition (3888, 3889), // &ldrusha -> &ldrushar + new Transition (3900, 4041), // &Left -> &Leftr + new Transition (3901, 3914), // &LeftA -> &LeftAr + new Transition (3906, 3907), // &LeftAngleB -> &LeftAngleBr + new Transition (3914, 3915), // &LeftAr -> &LeftArr + new Transition (3919, 3920), // &Lefta -> &Leftar + new Transition (3920, 3921), // &Leftar -> &Leftarr + new Transition (3926, 4052), // &left -> &leftr + new Transition (3927, 3928), // &lefta -> &leftar + new Transition (3928, 3929), // &leftar -> &leftarr + new Transition (3934, 3935), // &LeftArrowBa -> &LeftArrowBar + new Transition (3942, 3943), // &LeftArrowRightA -> &LeftArrowRightAr + new Transition (3943, 3944), // &LeftArrowRightAr -> &LeftArrowRightArr + new Transition (3967, 3968), // &LeftDoubleB -> &LeftDoubleBr + new Transition (3984, 3985), // &LeftDownTeeVecto -> &LeftDownTeeVector + new Transition (3991, 3992), // &LeftDownVecto -> &LeftDownVector + new Transition (3995, 3996), // &LeftDownVectorBa -> &LeftDownVectorBar + new Transition (4001, 4002), // &LeftFloo -> &LeftFloor + new Transition (4005, 4006), // &leftha -> &lefthar + new Transition (4023, 4024), // &leftlefta -> &leftleftar + new Transition (4024, 4025), // &leftleftar -> &leftleftarr + new Transition (4035, 4036), // &LeftRightA -> &LeftRightAr + new Transition (4036, 4037), // &LeftRightAr -> &LeftRightArr + new Transition (4046, 4047), // &Leftrighta -> &Leftrightar + new Transition (4047, 4048), // &Leftrightar -> &Leftrightarr + new Transition (4057, 4058), // &leftrighta -> &leftrightar + new Transition (4058, 4059), // &leftrightar -> &leftrightarr + new Transition (4066, 4067), // &leftrightha -> &leftrighthar + new Transition (4079, 4080), // &leftrightsquiga -> &leftrightsquigar + new Transition (4080, 4081), // &leftrightsquigar -> &leftrightsquigarr + new Transition (4089, 4090), // &LeftRightVecto -> &LeftRightVector + new Transition (4092, 4120), // &LeftT -> &LeftTr + new Transition (4096, 4097), // &LeftTeeA -> &LeftTeeAr + new Transition (4097, 4098), // &LeftTeeAr -> &LeftTeeArr + new Transition (4106, 4107), // &LeftTeeVecto -> &LeftTeeVector + new Transition (4110, 4111), // &leftth -> &leftthr + new Transition (4129, 4130), // &LeftTriangleBa -> &LeftTriangleBar + new Transition (4148, 4149), // &LeftUpDownVecto -> &LeftUpDownVector + new Transition (4158, 4159), // &LeftUpTeeVecto -> &LeftUpTeeVector + new Transition (4165, 4166), // &LeftUpVecto -> &LeftUpVector + new Transition (4169, 4170), // &LeftUpVectorBa -> &LeftUpVectorBar + new Transition (4176, 4177), // &LeftVecto -> &LeftVector + new Transition (4180, 4181), // &LeftVectorBa -> &LeftVectorBar + new Transition (4206, 4208), // &lesdoto -> &lesdotor + new Transition (4218, 4219), // &lessapp -> &lessappr + new Transition (4230, 4231), // &lesseqgt -> &lesseqgtr + new Transition (4235, 4236), // &lesseqqgt -> &lesseqqgtr + new Transition (4245, 4246), // &LessEqualG -> &LessEqualGr + new Transition (4250, 4251), // &LessEqualGreate -> &LessEqualGreater + new Transition (4263, 4264), // &LessG -> &LessGr + new Transition (4268, 4269), // &LessGreate -> &LessGreater + new Transition (4272, 4273), // &lessgt -> &lessgtr + new Transition (4301, 4315), // &lf -> &lfr + new Transition (4309, 4310), // &lfloo -> &lfloor + new Transition (4312, 4313), // &Lf -> &Lfr + new Transition (4322, 4323), // &lHa -> &lHar + new Transition (4326, 4327), // &lha -> &lhar + new Transition (4350, 4351), // &lla -> &llar + new Transition (4351, 4352), // &llar -> &llarr + new Transition (4355, 4356), // &llco -> &llcor + new Transition (4358, 4359), // &llcorne -> &llcorner + new Transition (4364, 4365), // &Llefta -> &Lleftar + new Transition (4365, 4366), // &Lleftar -> &Lleftarr + new Transition (4371, 4372), // &llha -> &llhar + new Transition (4375, 4376), // &llt -> &lltr + new Transition (4405, 4406), // &lnapp -> &lnappr + new Transition (4423, 4427), // &loa -> &loar + new Transition (4427, 4428), // &loar -> &loarr + new Transition (4430, 4431), // &lob -> &lobr + new Transition (4436, 4520), // &Long -> &Longr + new Transition (4441, 4442), // &LongLeftA -> &LongLeftAr + new Transition (4442, 4443), // &LongLeftAr -> &LongLeftArr + new Transition (4450, 4480), // &Longleft -> &Longleftr + new Transition (4451, 4452), // &Longlefta -> &Longleftar + new Transition (4452, 4453), // &Longleftar -> &Longleftarr + new Transition (4458, 4531), // &long -> &longr + new Transition (4462, 4491), // &longleft -> &longleftr + new Transition (4463, 4464), // &longlefta -> &longleftar + new Transition (4464, 4465), // &longleftar -> &longleftarr + new Transition (4474, 4475), // &LongLeftRightA -> &LongLeftRightAr + new Transition (4475, 4476), // &LongLeftRightAr -> &LongLeftRightArr + new Transition (4485, 4486), // &Longleftrighta -> &Longleftrightar + new Transition (4486, 4487), // &Longleftrightar -> &Longleftrightarr + new Transition (4496, 4497), // &longleftrighta -> &longleftrightar + new Transition (4497, 4498), // &longleftrightar -> &longleftrightarr + new Transition (4514, 4515), // &LongRightA -> &LongRightAr + new Transition (4515, 4516), // &LongRightAr -> &LongRightArr + new Transition (4525, 4526), // &Longrighta -> &Longrightar + new Transition (4526, 4527), // &Longrightar -> &Longrightarr + new Transition (4536, 4537), // &longrighta -> &longrightar + new Transition (4537, 4538), // &longrightar -> &longrightarr + new Transition (4544, 4545), // &loopa -> &loopar + new Transition (4545, 4546), // &loopar -> &looparr + new Transition (4548, 4554), // &looparrow -> &looparrowr + new Transition (4561, 4562), // &lopa -> &lopar + new Transition (4585, 4586), // &lowba -> &lowbar + new Transition (4589, 4590), // &Lowe -> &Lower + new Transition (4595, 4596), // &LowerLeftA -> &LowerLeftAr + new Transition (4596, 4597), // &LowerLeftAr -> &LowerLeftArr + new Transition (4606, 4607), // &LowerRightA -> &LowerRightAr + new Transition (4607, 4608), // &LowerRightAr -> &LowerRightArr + new Transition (4622, 4623), // &lpa -> &lpar + new Transition (4629, 4630), // &lra -> &lrar + new Transition (4630, 4631), // &lrar -> &lrarr + new Transition (4634, 4635), // &lrco -> &lrcor + new Transition (4637, 4638), // &lrcorne -> &lrcorner + new Transition (4641, 4642), // &lrha -> &lrhar + new Transition (4648, 4649), // &lrt -> &lrtr + new Transition (4659, 4660), // &Lsc -> &Lscr + new Transition (4662, 4663), // &lsc -> &lscr + new Transition (4680, 4682), // &lsquo -> &lsquor + new Transition (4684, 4685), // &Lst -> &Lstr + new Transition (4689, 4690), // &lst -> &lstr + new Transition (4698, 4731), // < -> <r + new Transition (4703, 4704), // <ci -> <cir + new Transition (4710, 4711), // <h -> <hr + new Transition (4721, 4722), // <la -> <lar + new Transition (4722, 4723), // <lar -> <larr + new Transition (4739, 4740), // <rPa -> <rPar + new Transition (4742, 4743), // &lu -> &lur + new Transition (4747, 4748), // &lurdsha -> &lurdshar + new Transition (4752, 4753), // &luruha -> &luruhar + new Transition (4756, 4757), // &lve -> &lver + new Transition (4768, 4804), // &ma -> &mar + new Transition (4769, 4770), // &mac -> ¯ + new Transition (4806, 4807), // &marke -> &marker + new Transition (4833, 4834), // &measu -> &measur + new Transition (4858, 4859), // &Mellint -> &Mellintr + new Transition (4862, 4863), // &Mf -> &Mfr + new Transition (4865, 4866), // &mf -> &mfr + new Transition (4872, 4873), // &mic -> &micr + new Transition (4883, 4884), // &midci -> &midcir + new Transition (4913, 4914), // &mld -> &mldr + new Transition (4938, 4939), // &Msc -> &Mscr + new Transition (4942, 4943), // &msc -> &mscr + new Transition (4965, 5855), // &n -> &nr + new Transition (4996, 4997), // &napp -> &nappr + new Transition (5002, 5003), // &natu -> &natur + new Transition (5021, 5030), // &nca -> &ncar + new Transition (5025, 5026), // &Nca -> &Ncar + new Transition (5066, 5067), // &nea -> &near + new Transition (5067, 5075), // &near -> &nearr + new Transition (5071, 5072), // &neA -> &neAr + new Transition (5072, 5073), // &neAr -> &neArr + new Transition (5122, 5123), // &NegativeVe -> &NegativeVer + new Transition (5142, 5143), // &nesea -> &nesear + new Transition (5152, 5153), // &NestedG -> &NestedGr + new Transition (5157, 5158), // &NestedGreate -> &NestedGreater + new Transition (5159, 5160), // &NestedGreaterG -> &NestedGreaterGr + new Transition (5164, 5165), // &NestedGreaterGreate -> &NestedGreaterGreater + new Transition (5189, 5190), // &Nf -> &Nfr + new Transition (5192, 5193), // &nf -> &nfr + new Transition (5221, 5223), // &ngt -> &ngtr + new Transition (5228, 5229), // &nhA -> &nhAr + new Transition (5229, 5230), // &nhAr -> &nhArr + new Transition (5232, 5233), // &nha -> &nhar + new Transition (5233, 5234), // &nhar -> &nharr + new Transition (5237, 5238), // &nhpa -> &nhpar + new Transition (5257, 5258), // &nlA -> &nlAr + new Transition (5258, 5259), // &nlAr -> &nlArr + new Transition (5261, 5262), // &nla -> &nlar + new Transition (5262, 5263), // &nlar -> &nlarr + new Transition (5265, 5266), // &nld -> &nldr + new Transition (5275, 5290), // &nLeft -> &nLeftr + new Transition (5276, 5277), // &nLefta -> &nLeftar + new Transition (5277, 5278), // &nLeftar -> &nLeftarr + new Transition (5283, 5301), // &nleft -> &nleftr + new Transition (5284, 5285), // &nlefta -> &nleftar + new Transition (5285, 5286), // &nleftar -> &nleftarr + new Transition (5295, 5296), // &nLeftrighta -> &nLeftrightar + new Transition (5296, 5297), // &nLeftrightar -> &nLeftrightarr + new Transition (5306, 5307), // &nleftrighta -> &nleftrightar + new Transition (5307, 5308), // &nleftrightar -> &nleftrightarr + new Transition (5334, 5336), // &nlt -> &nltr + new Transition (5348, 5349), // &NoB -> &NoBr + new Transition (5355, 5356), // &NonB -> &NonBr + new Transition (5383, 5384), // &NotCong -> &NotCongr + new Transition (5403, 5404), // &NotDoubleVe -> &NotDoubleVer + new Transition (5411, 5412), // &NotDoubleVerticalBa -> &NotDoubleVerticalBar + new Transition (5439, 5440), // &NotG -> &NotGr + new Transition (5444, 5445), // &NotGreate -> &NotGreater + new Transition (5463, 5464), // &NotGreaterG -> &NotGreaterGr + new Transition (5468, 5469), // &NotGreaterGreate -> &NotGreaterGreater + new Transition (5532, 5533), // &NotLeftT -> &NotLeftTr + new Transition (5542, 5543), // &NotLeftTriangleBa -> &NotLeftTriangleBar + new Transition (5560, 5561), // &NotLessG -> &NotLessGr + new Transition (5565, 5566), // &NotLessGreate -> &NotLessGreater + new Transition (5596, 5597), // &NotNestedG -> &NotNestedGr + new Transition (5601, 5602), // &NotNestedGreate -> &NotNestedGreater + new Transition (5603, 5604), // &NotNestedGreaterG -> &NotNestedGreaterGr + new Transition (5608, 5609), // &NotNestedGreaterGreate -> &NotNestedGreaterGreater + new Transition (5630, 5631), // &NotP -> &NotPr + new Transition (5659, 5660), // &NotReve -> &NotRever + new Transition (5675, 5676), // &NotRightT -> &NotRightTr + new Transition (5685, 5686), // &NotRightTriangleBa -> &NotRightTriangleBar + new Transition (5697, 5698), // &NotSqua -> &NotSquar + new Transition (5714, 5715), // &NotSquareSupe -> &NotSquareSuper + new Transition (5769, 5770), // &NotSupe -> &NotSuper + new Transition (5810, 5811), // &NotVe -> &NotVer + new Transition (5818, 5819), // &NotVerticalBa -> &NotVerticalBar + new Transition (5821, 5842), // &np -> &npr + new Transition (5822, 5823), // &npa -> &npar + new Transition (5856, 5857), // &nrA -> &nrAr + new Transition (5857, 5858), // &nrAr -> &nrArr + new Transition (5860, 5861), // &nra -> &nrar + new Transition (5861, 5862), // &nrar -> &nrarr + new Transition (5873, 5874), // &nRighta -> &nRightar + new Transition (5874, 5875), // &nRightar -> &nRightarr + new Transition (5883, 5884), // &nrighta -> &nrightar + new Transition (5884, 5885), // &nrightar -> &nrightarr + new Transition (5889, 5890), // &nrt -> &nrtr + new Transition (5896, 5908), // &nsc -> &nscr + new Transition (5905, 5906), // &Nsc -> &Nscr + new Transition (5911, 5912), // &nsho -> &nshor + new Transition (5919, 5920), // &nshortpa -> &nshortpar + new Transition (5939, 5940), // &nspa -> &nspar + new Transition (5988, 6006), // &nt -> &ntr + new Transition (6012, 6021), // &ntriangle -> &ntriangler + new Transition (6036, 6037), // &nume -> &numer + new Transition (6043, 6097), // &nv -> &nvr + new Transition (6074, 6075), // &nvHa -> &nvHar + new Transition (6075, 6076), // &nvHar -> &nvHarr + new Transition (6085, 6086), // &nvlA -> &nvlAr + new Transition (6086, 6087), // &nvlAr -> &nvlArr + new Transition (6091, 6093), // &nvlt -> &nvltr + new Transition (6098, 6099), // &nvrA -> &nvrAr + new Transition (6099, 6100), // &nvrAr -> &nvrArr + new Transition (6102, 6103), // &nvrt -> &nvrtr + new Transition (6112, 6113), // &nwa -> &nwar + new Transition (6113, 6121), // &nwar -> &nwarr + new Transition (6117, 6118), // &nwA -> &nwAr + new Transition (6118, 6119), // &nwAr -> &nwArr + new Transition (6128, 6129), // &nwnea -> &nwnear + new Transition (6131, 6340), // &O -> &Or + new Transition (6138, 6342), // &o -> &or + new Transition (6149, 6150), // &oci -> &ocir + new Transition (6153, 6154), // &Oci -> &Ocir + new Transition (6200, 6208), // &of -> &ofr + new Transition (6202, 6203), // &ofci -> &ofcir + new Transition (6205, 6206), // &Of -> &Ofr + new Transition (6210, 6220), // &og -> &ogr + new Transition (6214, 6215), // &Og -> &Ogr + new Transition (6229, 6230), // &ohba -> &ohbar + new Transition (6239, 6240), // &ola -> &olar + new Transition (6240, 6241), // &olar -> &olarr + new Transition (6243, 6247), // &olc -> &olcr + new Transition (6244, 6245), // &olci -> &olcir + new Transition (6260, 6261), // &Omac -> &Omacr + new Transition (6265, 6266), // &omac -> &omacr + new Transition (6277, 6278), // &Omic -> &Omicr + new Transition (6283, 6284), // &omic -> &omicr + new Transition (6303, 6304), // &opa -> &opar + new Transition (6310, 6311), // &OpenCu -> &OpenCur + new Transition (6332, 6333), // &ope -> &oper + new Transition (6344, 6345), // &ora -> &orar + new Transition (6345, 6346), // &orar -> &orarr + new Transition (6350, 6351), // &orde -> &order + new Transition (6365, 6366), // &oro -> &oror + new Transition (6379, 6380), // &Osc -> &Oscr + new Transition (6383, 6384), // &osc -> &oscr + new Transition (6432, 6433), // &ovba -> &ovbar + new Transition (6436, 6437), // &Ove -> &Over + new Transition (6438, 6442), // &OverB -> &OverBr + new Transition (6439, 6440), // &OverBa -> &OverBar + new Transition (6452, 6453), // &OverPa -> &OverPar + new Transition (6463, 6642), // &p -> &pr + new Transition (6464, 6465), // &pa -> &par + new Transition (6482, 6640), // &P -> &Pr + new Transition (6483, 6484), // &Pa -> &Par + new Transition (6497, 6498), // &pe -> &per + new Transition (6518, 6519), // &Pf -> &Pfr + new Transition (6521, 6522), // &pf -> &pfr + new Transition (6549, 6550), // &pitchfo -> &pitchfor + new Transition (6571, 6572), // &plusaci -> &plusacir + new Transition (6577, 6578), // &plusci -> &pluscir + new Transition (6613, 6614), // &Poinca -> &Poincar + new Transition (6659, 6660), // &precapp -> &precappr + new Transition (6665, 6666), // &preccu -> &preccur + new Transition (6708, 6709), // &precnapp -> &precnappr + new Transition (6757, 6758), // &profala -> &profalar + new Transition (6766, 6767), // &profsu -> &profsur + new Transition (6773, 6774), // &Propo -> &Propor + new Transition (6790, 6791), // &pru -> &prur + new Transition (6796, 6797), // &Psc -> &Pscr + new Transition (6800, 6801), // &psc -> &pscr + new Transition (6814, 6815), // &Qf -> &Qfr + new Transition (6818, 6819), // &qf -> &qfr + new Transition (6833, 6834), // &qp -> &qpr + new Transition (6840, 6841), // &Qsc -> &Qscr + new Transition (6844, 6845), // &qsc -> &qscr + new Transition (6850, 6851), // &quate -> &quater + new Transition (6876, 7526), // &r -> &rr + new Transition (6877, 6928), // &rA -> &rAr + new Transition (6878, 6879), // &rAa -> &rAar + new Transition (6879, 6880), // &rAar -> &rAarr + new Transition (6882, 6931), // &ra -> &rar + new Transition (6886, 7531), // &R -> &Rr + new Transition (6887, 6925), // &Ra -> &Rar + new Transition (6925, 6926), // &Rar -> &Rarr + new Transition (6928, 6929), // &rAr -> &rArr + new Transition (6931, 6932), // &rar -> &rarr + new Transition (6987, 6988), // &RBa -> &RBar + new Transition (6988, 6989), // &RBar -> &RBarr + new Transition (6992, 6993), // &rBa -> &rBar + new Transition (6993, 6994), // &rBar -> &rBarr + new Transition (6996, 7005), // &rb -> &rbr + new Transition (6997, 6998), // &rba -> &rbar + new Transition (6998, 6999), // &rbar -> &rbarr + new Transition (7001, 7002), // &rbb -> &rbbr + new Transition (7022, 7023), // &Rca -> &Rcar + new Transition (7028, 7029), // &rca -> &rcar + new Transition (7060, 7061), // &rdldha -> &rdldhar + new Transition (7065, 7067), // &rdquo -> &rdquor + new Transition (7083, 7084), // &realpa -> &realpar + new Transition (7098, 7099), // &Reve -> &Rever + new Transition (7115, 7116), // &ReverseEquilib -> &ReverseEquilibr + new Transition (7129, 7130), // &ReverseUpEquilib -> &ReverseUpEquilibr + new Transition (7135, 7149), // &rf -> &rfr + new Transition (7143, 7144), // &rfloo -> &rfloor + new Transition (7146, 7147), // &Rf -> &Rfr + new Transition (7152, 7153), // &rHa -> &rHar + new Transition (7156, 7157), // &rha -> &rhar + new Transition (7175, 7188), // &RightA -> &RightAr + new Transition (7180, 7181), // &RightAngleB -> &RightAngleBr + new Transition (7188, 7189), // &RightAr -> &RightArr + new Transition (7193, 7194), // &Righta -> &Rightar + new Transition (7194, 7195), // &Rightar -> &Rightarr + new Transition (7202, 7314), // &right -> &rightr + new Transition (7203, 7204), // &righta -> &rightar + new Transition (7204, 7205), // &rightar -> &rightarr + new Transition (7210, 7211), // &RightArrowBa -> &RightArrowBar + new Transition (7217, 7218), // &RightArrowLeftA -> &RightArrowLeftAr + new Transition (7218, 7219), // &RightArrowLeftAr -> &RightArrowLeftArr + new Transition (7242, 7243), // &RightDoubleB -> &RightDoubleBr + new Transition (7259, 7260), // &RightDownTeeVecto -> &RightDownTeeVector + new Transition (7266, 7267), // &RightDownVecto -> &RightDownVector + new Transition (7270, 7271), // &RightDownVectorBa -> &RightDownVectorBar + new Transition (7276, 7277), // &RightFloo -> &RightFloor + new Transition (7280, 7281), // &rightha -> &righthar + new Transition (7298, 7299), // &rightlefta -> &rightleftar + new Transition (7299, 7300), // &rightleftar -> &rightleftarr + new Transition (7306, 7307), // &rightleftha -> &rightlefthar + new Transition (7319, 7320), // &rightrighta -> &rightrightar + new Transition (7320, 7321), // &rightrightar -> &rightrightarr + new Transition (7331, 7332), // &rightsquiga -> &rightsquigar + new Transition (7332, 7333), // &rightsquigar -> &rightsquigarr + new Transition (7337, 7365), // &RightT -> &RightTr + new Transition (7341, 7342), // &RightTeeA -> &RightTeeAr + new Transition (7342, 7343), // &RightTeeAr -> &RightTeeArr + new Transition (7351, 7352), // &RightTeeVecto -> &RightTeeVector + new Transition (7355, 7356), // &rightth -> &rightthr + new Transition (7374, 7375), // &RightTriangleBa -> &RightTriangleBar + new Transition (7393, 7394), // &RightUpDownVecto -> &RightUpDownVector + new Transition (7403, 7404), // &RightUpTeeVecto -> &RightUpTeeVector + new Transition (7410, 7411), // &RightUpVecto -> &RightUpVector + new Transition (7414, 7415), // &RightUpVectorBa -> &RightUpVectorBar + new Transition (7421, 7422), // &RightVecto -> &RightVector + new Transition (7425, 7426), // &RightVectorBa -> &RightVectorBar + new Transition (7443, 7444), // &rla -> &rlar + new Transition (7444, 7445), // &rlar -> &rlarr + new Transition (7448, 7449), // &rlha -> &rlhar + new Transition (7470, 7474), // &roa -> &roar + new Transition (7474, 7475), // &roar -> &roarr + new Transition (7477, 7478), // &rob -> &robr + new Transition (7482, 7483), // &ropa -> &ropar + new Transition (7513, 7514), // &rpa -> &rpar + new Transition (7527, 7528), // &rra -> &rrar + new Transition (7528, 7529), // &rrar -> &rrarr + new Transition (7536, 7537), // &Rrighta -> &Rrightar + new Transition (7537, 7538), // &Rrightar -> &Rrightarr + new Transition (7549, 7550), // &Rsc -> &Rscr + new Transition (7552, 7553), // &rsc -> &rscr + new Transition (7563, 7565), // &rsquo -> &rsquor + new Transition (7567, 7578), // &rt -> &rtr + new Transition (7568, 7569), // &rth -> &rthr + new Transition (7586, 7587), // &rtrilt -> &rtriltr + new Transition (7605, 7606), // &ruluha -> &ruluhar + new Transition (7617, 8068), // &s -> &sr + new Transition (7633, 7641), // &sca -> &scar + new Transition (7636, 7637), // &Sca -> &Scar + new Transition (7662, 7663), // &Sci -> &Scir + new Transition (7666, 7667), // &sci -> &scir + new Transition (7704, 7705), // &sea -> &sear + new Transition (7705, 7713), // &sear -> &searr + new Transition (7709, 7710), // &seA -> &seAr + new Transition (7710, 7711), // &seAr -> &seArr + new Transition (7726, 7727), // &seswa -> &seswar + new Transition (7741, 7742), // &Sf -> &Sfr + new Transition (7744, 7745), // &sf -> &sfr + new Transition (7752, 7753), // &sha -> &shar + new Transition (7773, 7774), // &Sho -> &Shor + new Transition (7780, 7781), // &ShortDownA -> &ShortDownAr + new Transition (7781, 7782), // &ShortDownAr -> &ShortDownArr + new Transition (7790, 7791), // &ShortLeftA -> &ShortLeftAr + new Transition (7791, 7792), // &ShortLeftAr -> &ShortLeftArr + new Transition (7796, 7797), // &sho -> &shor + new Transition (7804, 7805), // &shortpa -> &shortpar + new Transition (7817, 7818), // &ShortRightA -> &ShortRightAr + new Transition (7818, 7819), // &ShortRightAr -> &ShortRightArr + new Transition (7825, 7826), // &ShortUpA -> &ShortUpAr + new Transition (7826, 7827), // &ShortUpAr -> &ShortUpArr + new Transition (7847, 7873), // &sim -> &simr + new Transition (7874, 7875), // &simra -> &simrar + new Transition (7875, 7876), // &simrar -> &simrarr + new Transition (7879, 7880), // &sla -> &slar + new Transition (7880, 7881), // &slar -> &slarr + new Transition (7888, 7889), // &SmallCi -> &SmallCir + new Transition (7913, 7914), // &smepa -> &smepar + new Transition (7946, 7947), // &solba -> &solbar + new Transition (7957, 7966), // &spa -> &spar + new Transition (7980, 7981), // &Sq -> &Sqr + new Transition (8011, 8012), // &Squa -> &Squar + new Transition (8015, 8016), // &squa -> &squar + new Transition (8022, 8023), // &SquareInte -> &SquareInter + new Transition (8046, 8047), // &SquareSupe -> &SquareSuper + new Transition (8069, 8070), // &sra -> &srar + new Transition (8070, 8071), // &srar -> &srarr + new Transition (8074, 8075), // &Ssc -> &Sscr + new Transition (8078, 8079), // &ssc -> &sscr + new Transition (8092, 8093), // &ssta -> &sstar + new Transition (8097, 8098), // &Sta -> &Star + new Transition (8100, 8106), // &st -> &str + new Transition (8101, 8102), // &sta -> &star + new Transition (8131, 8160), // &sub -> &subr + new Transition (8161, 8162), // &subra -> &subrar + new Transition (8162, 8163), // &subrar -> &subrarr + new Transition (8203, 8204), // &succapp -> &succappr + new Transition (8209, 8210), // &succcu -> &succcur + new Transition (8252, 8253), // &succnapp -> &succnappr + new Transition (8308, 8309), // &Supe -> &Super + new Transition (8329, 8330), // &supla -> &suplar + new Transition (8330, 8331), // &suplar -> &suplarr + new Transition (8376, 8377), // &swa -> &swar + new Transition (8377, 8385), // &swar -> &swarr + new Transition (8381, 8382), // &swA -> &swAr + new Transition (8382, 8383), // &swAr -> &swArr + new Transition (8392, 8393), // &swnwa -> &swnwar + new Transition (8400, 8676), // &T -> &Tr + new Transition (8404, 8628), // &t -> &tr + new Transition (8405, 8406), // &ta -> &tar + new Transition (8415, 8416), // &tb -> &tbr + new Transition (8420, 8421), // &Tca -> &Tcar + new Transition (8426, 8427), // &tca -> &tcar + new Transition (8450, 8451), // &tel -> &telr + new Transition (8455, 8456), // &Tf -> &Tfr + new Transition (8458, 8459), // &tf -> &tfr + new Transition (8462, 8463), // &the -> &ther + new Transition (8468, 8469), // &The -> &Ther + new Transition (8472, 8473), // &Therefo -> &Therefor + new Transition (8477, 8478), // &therefo -> &therefor + new Transition (8498, 8499), // &thickapp -> &thickappr + new Transition (8540, 8541), // &tho -> &thor + new Transition (8582, 8583), // ×ba -> ×bar + new Transition (8601, 8602), // &topci -> &topcir + new Transition (8610, 8611), // &topfo -> &topfor + new Transition (8617, 8618), // &tp -> &tpr + new Transition (8638, 8655), // &triangle -> &triangler + new Transition (8706, 8707), // &Tsc -> &Tscr + new Transition (8710, 8711), // &tsc -> &tscr + new Transition (8727, 8728), // &Tst -> &Tstr + new Transition (8732, 8733), // &tst -> &tstr + new Transition (8746, 8757), // &twohead -> &twoheadr + new Transition (8751, 8752), // &twoheadlefta -> &twoheadleftar + new Transition (8752, 8753), // &twoheadleftar -> &twoheadleftarr + new Transition (8762, 8763), // &twoheadrighta -> &twoheadrightar + new Transition (8763, 8764), // &twoheadrightar -> &twoheadrightarr + new Transition (8768, 9140), // &U -> &Ur + new Transition (8769, 8782), // &Ua -> &Uar + new Transition (8775, 9127), // &u -> &ur + new Transition (8776, 8789), // &ua -> &uar + new Transition (8782, 8783), // &Uar -> &Uarr + new Transition (8785, 8786), // &uA -> &uAr + new Transition (8786, 8787), // &uAr -> &uArr + new Transition (8789, 8790), // &uar -> &uarr + new Transition (8794, 8795), // &Uarroci -> &Uarrocir + new Transition (8797, 8798), // &Ub -> &Ubr + new Transition (8802, 8803), // &ub -> &ubr + new Transition (8816, 8817), // &Uci -> &Ucir + new Transition (8821, 8822), // &uci -> &ucir + new Transition (8830, 8831), // &uda -> &udar + new Transition (8831, 8832), // &udar -> &udarr + new Transition (8846, 8847), // &udha -> &udhar + new Transition (8849, 8858), // &uf -> &ufr + new Transition (8855, 8856), // &Uf -> &Ufr + new Transition (8860, 8861), // &Ug -> &Ugr + new Transition (8866, 8867), // &ug -> &ugr + new Transition (8873, 8874), // &uHa -> &uHar + new Transition (8877, 8878), // &uha -> &uhar + new Transition (8878, 8881), // &uhar -> &uharr + new Transition (8888, 8896), // &ulc -> &ulcr + new Transition (8889, 8890), // &ulco -> &ulcor + new Transition (8893, 8894), // &ulcorne -> &ulcorner + new Transition (8900, 8901), // &ult -> &ultr + new Transition (8906, 8907), // &Umac -> &Umacr + new Transition (8911, 8912), // &umac -> &umacr + new Transition (8918, 8919), // &Unde -> &Under + new Transition (8920, 8924), // &UnderB -> &UnderBr + new Transition (8921, 8922), // &UnderBa -> &UnderBar + new Transition (8934, 8935), // &UnderPa -> &UnderPar + new Transition (8971, 8972), // &UpA -> &UpAr + new Transition (8972, 8973), // &UpAr -> &UpArr + new Transition (8977, 8978), // &Upa -> &Upar + new Transition (8978, 8979), // &Upar -> &Uparr + new Transition (8984, 8985), // &upa -> &upar + new Transition (8985, 8986), // &upar -> &uparr + new Transition (8991, 8992), // &UpArrowBa -> &UpArrowBar + new Transition (8998, 8999), // &UpArrowDownA -> &UpArrowDownAr + new Transition (8999, 9000), // &UpArrowDownAr -> &UpArrowDownArr + new Transition (9008, 9009), // &UpDownA -> &UpDownAr + new Transition (9009, 9010), // &UpDownAr -> &UpDownArr + new Transition (9018, 9019), // &Updowna -> &Updownar + new Transition (9019, 9020), // &Updownar -> &Updownarr + new Transition (9028, 9029), // &updowna -> &updownar + new Transition (9029, 9030), // &updownar -> &updownarr + new Transition (9040, 9041), // &UpEquilib -> &UpEquilibr + new Transition (9047, 9048), // &upha -> &uphar + new Transition (9052, 9058), // &upharpoon -> &upharpoonr + new Transition (9069, 9070), // &Uppe -> &Upper + new Transition (9075, 9076), // &UpperLeftA -> &UpperLeftAr + new Transition (9076, 9077), // &UpperLeftAr -> &UpperLeftArr + new Transition (9086, 9087), // &UpperRightA -> &UpperRightAr + new Transition (9087, 9088), // &UpperRightAr -> &UpperRightArr + new Transition (9112, 9113), // &UpTeeA -> &UpTeeAr + new Transition (9113, 9114), // &UpTeeAr -> &UpTeeArr + new Transition (9120, 9121), // &upupa -> &upupar + new Transition (9121, 9122), // &upupar -> &upuparr + new Transition (9128, 9136), // &urc -> &urcr + new Transition (9129, 9130), // &urco -> &urcor + new Transition (9133, 9134), // &urcorne -> &urcorner + new Transition (9149, 9150), // &urt -> &urtr + new Transition (9154, 9155), // &Usc -> &Uscr + new Transition (9158, 9159), // &usc -> &uscr + new Transition (9161, 9177), // &ut -> &utr + new Transition (9183, 9184), // &uua -> &uuar + new Transition (9184, 9185), // &uuar -> &uuarr + new Transition (9201, 9445), // &v -> &vr + new Transition (9202, 9208), // &va -> &var + new Transition (9204, 9205), // &vang -> &vangr + new Transition (9208, 9247), // &var -> &varr + new Transition (9231, 9237), // &varp -> &varpr + new Transition (9243, 9244), // &vA -> &vAr + new Transition (9244, 9245), // &vAr -> &vArr + new Transition (9279, 9285), // &vart -> &vartr + new Transition (9291, 9297), // &vartriangle -> &vartriangler + new Transition (9305, 9306), // &Vba -> &Vbar + new Transition (9309, 9310), // &vBa -> &vBar + new Transition (9342, 9360), // &Ve -> &Ver + new Transition (9345, 9365), // &ve -> &ver + new Transition (9349, 9350), // &veeba -> &veebar + new Transition (9362, 9363), // &Verba -> &Verbar + new Transition (9367, 9368), // &verba -> &verbar + new Transition (9379, 9380), // &VerticalBa -> &VerticalBar + new Transition (9390, 9391), // &VerticalSepa -> &VerticalSepar + new Transition (9394, 9395), // &VerticalSeparato -> &VerticalSeparator + new Transition (9414, 9415), // &Vf -> &Vfr + new Transition (9417, 9418), // &vf -> &vfr + new Transition (9421, 9422), // &vlt -> &vltr + new Transition (9440, 9441), // &vp -> &vpr + new Transition (9446, 9447), // &vrt -> &vrtr + new Transition (9451, 9452), // &Vsc -> &Vscr + new Transition (9455, 9456), // &vsc -> &vscr + new Transition (9486, 9487), // &Wci -> &Wcir + new Transition (9490, 9533), // &w -> &wr + new Transition (9492, 9493), // &wci -> &wcir + new Transition (9499, 9500), // &wedba -> &wedbar + new Transition (9513, 9514), // &weie -> &weier + new Transition (9517, 9518), // &Wf -> &Wfr + new Transition (9520, 9521), // &wf -> &wfr + new Transition (9541, 9542), // &Wsc -> &Wscr + new Transition (9545, 9546), // &wsc -> &wscr + new Transition (9548, 9623), // &x -> &xr + new Transition (9553, 9554), // &xci -> &xcir + new Transition (9561, 9562), // &xdt -> &xdtr + new Transition (9566, 9567), // &Xf -> &Xfr + new Transition (9569, 9570), // &xf -> &xfr + new Transition (9573, 9574), // &xhA -> &xhAr + new Transition (9574, 9575), // &xhAr -> &xhArr + new Transition (9577, 9578), // &xha -> &xhar + new Transition (9578, 9579), // &xhar -> &xharr + new Transition (9586, 9587), // &xlA -> &xlAr + new Transition (9587, 9588), // &xlAr -> &xlArr + new Transition (9590, 9591), // &xla -> &xlar + new Transition (9591, 9592), // &xlar -> &xlarr + new Transition (9624, 9625), // &xrA -> &xrAr + new Transition (9625, 9626), // &xrAr -> &xrArr + new Transition (9628, 9629), // &xra -> &xrar + new Transition (9629, 9630), // &xrar -> &xrarr + new Transition (9633, 9634), // &Xsc -> &Xscr + new Transition (9637, 9638), // &xsc -> &xscr + new Transition (9651, 9652), // &xut -> &xutr + new Transition (9686, 9687), // &Yci -> &Ycir + new Transition (9691, 9692), // &yci -> &ycir + new Transition (9702, 9703), // &Yf -> &Yfr + new Transition (9705, 9706), // &yf -> &yfr + new Transition (9725, 9726), // &Ysc -> &Yscr + new Transition (9729, 9730), // &ysc -> &yscr + new Transition (9762, 9763), // &Zca -> &Zcar + new Transition (9768, 9769), // &zca -> &zcar + new Transition (9787, 9788), // &zeet -> &zeetr + new Transition (9791, 9792), // &Ze -> &Zer + new Transition (9811, 9812), // &Zf -> &Zfr + new Transition (9814, 9815), // &zf -> &zfr + new Transition (9826, 9827), // &zig -> &zigr + new Transition (9828, 9829), // &zigra -> &zigrar + new Transition (9829, 9830), // &zigrar -> &zigrarr + new Transition (9841, 9842), // &Zsc -> &Zscr + new Transition (9845, 9846) // &zsc -> &zscr + }; + TransitionTable_s = new Transition[368] { + new Transition (0, 7617), // & -> &s + new Transition (1, 247), // &A -> &As + new Transition (8, 251), // &a -> &as + new Transition (81, 82), // &alef -> &alefs + new Transition (120, 128), // &and -> &ands + new Transition (136, 172), // &ang -> &angs + new Transition (143, 144), // &angm -> &angms + new Transition (213, 214), // &apo -> &apos + new Transition (247, 255), // &As -> &Ass + new Transition (301, 744), // &b -> &bs + new Transition (304, 324), // &back -> &backs + new Transition (311, 312), // &backep -> &backeps + new Transition (331, 740), // &B -> &Bs + new Transition (334, 335), // &Back -> &Backs + new Transition (337, 338), // &Backsla -> &Backslas + new Transition (387, 388), // &becau -> &becaus + new Transition (393, 394), // &Becau -> &Becaus + new Transition (405, 406), // &bep -> &beps + new Transition (420, 421), // &Bernoulli -> &Bernoullis + new Transition (443, 471), // &big -> &bigs + new Transition (462, 463), // &bigoplu -> &bigoplus + new Transition (468, 469), // &bigotime -> &bigotimes + new Transition (500, 501), // &biguplu -> &biguplus + new Transition (522, 531), // &black -> &blacks + new Transition (659, 660), // &boxminu -> &boxminus + new Transition (664, 665), // &boxplu -> &boxplus + new Transition (670, 671), // &boxtime -> &boxtimes + new Transition (762, 763), // &bsolh -> &bsolhs + new Transition (789, 1270), // &C -> &Cs + new Transition (796, 1274), // &c -> &cs + new Transition (805, 846), // &cap -> &caps + new Transition (858, 859), // &Cayley -> &Cayleys + new Transition (863, 864), // &ccap -> &ccaps + new Transition (901, 902), // &ccup -> &ccups + new Transition (902, 904), // &ccups -> &ccupss + new Transition (979, 1063), // &cir -> &cirs + new Transition (1005, 1006), // &circleda -> &circledas + new Transition (1015, 1016), // &circledda -> &circleddas + new Transition (1035, 1036), // &CircleMinu -> &CircleMinus + new Transition (1040, 1041), // &CirclePlu -> &CirclePlus + new Transition (1046, 1047), // &CircleTime -> &CircleTimes + new Transition (1069, 1092), // &Clo -> &Clos + new Transition (1073, 1074), // &Clockwi -> &Clockwis + new Transition (1119, 1120), // &club -> &clubs + new Transition (1161, 1162), // &complexe -> &complexes + new Transition (1221, 1223), // © -> ©s + new Transition (1237, 1238), // &CounterClockwi -> &CounterClockwis + new Transition (1262, 1263), // &Cro -> &Cros + new Transition (1263, 1264), // &Cros -> &Cross + new Transition (1266, 1267), // &cro -> &cros + new Transition (1267, 1268), // &cros -> &cross + new Transition (1301, 1305), // &cue -> &cues + new Transition (1318, 1344), // &cup -> &cups + new Transition (1356, 1362), // &curlyeq -> &curlyeqs + new Transition (1425, 2040), // &D -> &Ds + new Transition (1426, 1457), // &Da -> &Das + new Transition (1432, 2044), // &d -> &ds + new Transition (1433, 1454), // &da -> &das + new Transition (1511, 1512), // &ddot -> &ddots + new Transition (1536, 1537), // &dfi -> &dfis + new Transition (1599, 1639), // &di -> &dis + new Transition (1601, 1617), // &diam -> &diams + new Transition (1610, 1612), // &diamond -> &diamonds + new Transition (1654, 1655), // ÷ontime -> ÷ontimes + new Transition (1694, 1724), // &dot -> &dots + new Transition (1716, 1717), // &dotminu -> &dotminus + new Transition (1721, 1722), // &dotplu -> &dotplus + new Transition (1929, 1930), // &downdownarrow -> &downdownarrows + new Transition (2108, 2418), // &E -> &Es + new Transition (2115, 2422), // &e -> &es + new Transition (2116, 2122), // &ea -> &eas + new Transition (2185, 2198), // &eg -> &egs + new Transition (2204, 2222), // &el -> &els + new Transition (2217, 2218), // &elinter -> &elinters + new Transition (2233, 2279), // &em -> &ems + new Transition (2240, 2242), // &empty -> &emptys + new Transition (2290, 2293), // &en -> &ens + new Transition (2312, 2323), // &ep -> &eps + new Transition (2314, 2316), // &epar -> &epars + new Transition (2320, 2321), // &eplu -> &eplus + new Transition (2326, 2327), // &Ep -> &Eps + new Transition (2339, 2350), // &eq -> &eqs + new Transition (2363, 2364), // &eqslantle -> &eqslantles + new Transition (2364, 2365), // &eqslantles -> &eqslantless + new Transition (2374, 2375), // &equal -> &equals + new Transition (2383, 2384), // &eque -> &eques + new Transition (2405, 2406), // &eqvpar -> &eqvpars + new Transition (2462, 2463), // &exi -> &exis + new Transition (2467, 2468), // &Exi -> &Exis + new Transition (2469, 2470), // &Exist -> &Exists + new Transition (2503, 2697), // &f -> &fs + new Transition (2512, 2513), // &fallingdot -> &fallingdots + new Transition (2517, 2693), // &F -> &Fs + new Transition (2601, 2602), // &fltn -> &fltns + new Transition (2648, 2686), // &fra -> &fras + new Transition (2701, 2927), // &g -> &gs + new Transition (2708, 2923), // &G -> &Gs + new Transition (2765, 2781), // &ge -> &ges + new Transition (2771, 2775), // &geq -> &geqs + new Transition (2796, 2797), // &gesle -> &gesles + new Transition (2832, 2849), // &gn -> &gns + new Transition (2879, 2880), // &GreaterEqualLe -> &GreaterEqualLes + new Transition (2880, 2881), // &GreaterEqualLes -> &GreaterEqualLess + new Transition (2902, 2903), // &GreaterLe -> &GreaterLes + new Transition (2903, 2904), // &GreaterLes -> &GreaterLess + new Transition (2961, 2962), // >que -> >ques + new Transition (2965, 2998), // >r -> >rs + new Transition (2983, 2984), // >reqle -> >reqles + new Transition (2984, 2985), // >reqles -> >reqless + new Transition (2989, 2990), // >reqqle -> >reqqles + new Transition (2990, 2991), // >reqqles -> >reqqless + new Transition (2994, 2995), // >rle -> >rles + new Transition (2995, 2996), // >rles -> >rless + new Transition (3014, 3184), // &H -> &Hs + new Transition (3020, 3188), // &h -> &hs + new Transition (3023, 3024), // &hair -> &hairs + new Transition (3077, 3078), // &heart -> &hearts + new Transition (3112, 3113), // &hk -> &hks + new Transition (3193, 3194), // &hsla -> &hslas + new Transition (3236, 3503), // &I -> &Is + new Transition (3243, 3507), // &i -> &is + new Transition (3375, 3376), // &Implie -> &Implies + new Transition (3410, 3411), // &integer -> &integers + new Transition (3424, 3425), // &Inter -> &Inters + new Transition (3445, 3446), // &Invi -> &Invis + new Transition (3460, 3461), // &InvisibleTime -> &InvisibleTimes + new Transition (3499, 3500), // &ique -> &iques + new Transition (3512, 3520), // &isin -> &isins + new Transition (3555, 3590), // &J -> &Js + new Transition (3561, 3594), // &j -> &js + new Transition (3618, 3684), // &K -> &Ks + new Transition (3624, 3688), // &k -> &ks + new Transition (3692, 4652), // &l -> &ls + new Transition (3698, 4658), // &L -> &Ls + new Transition (3766, 3785), // &larr -> &larrs + new Transition (3770, 3771), // &larrbf -> &larrbfs + new Transition (3773, 3774), // &larrf -> &larrfs + new Transition (3803, 3805), // &late -> &lates + new Transition (3828, 3831), // &lbrk -> &lbrks + new Transition (3869, 3891), // &ld -> &lds + new Transition (3885, 3886), // &ldru -> &ldrus + new Transition (3896, 4197), // &le -> &les + new Transition (3898, 4238), // &Le -> &Les + new Transition (4027, 4028), // &leftleftarrow -> &leftleftarrows + new Transition (4056, 4074), // &leftright -> &leftrights + new Transition (4061, 4063), // &leftrightarrow -> &leftrightarrows + new Transition (4071, 4072), // &leftrightharpoon -> &leftrightharpoons + new Transition (4117, 4118), // &leftthreetime -> &leftthreetimes + new Transition (4187, 4191), // &leq -> &leqs + new Transition (4197, 4215), // &les -> &less + new Transition (4212, 4213), // &lesge -> &lesges + new Transition (4215, 4280), // &less -> &lesss + new Transition (4238, 4239), // &Les -> &Less + new Transition (4276, 4277), // &LessLe -> &LessLes + new Transition (4277, 4278), // &LessLes -> &LessLess + new Transition (4302, 4303), // &lfi -> &lfis + new Transition (4392, 4393), // &lmou -> &lmous + new Transition (4401, 4418), // &ln -> &lns + new Transition (4504, 4505), // &longmap -> &longmaps + new Transition (4570, 4571), // &loplu -> &loplus + new Transition (4576, 4577), // &lotime -> &lotimes + new Transition (4580, 4581), // &lowa -> &lowas + new Transition (4717, 4718), // <ime -> <imes + new Transition (4727, 4728), // <que -> <ques + new Transition (4744, 4745), // &lurd -> &lurds + new Transition (4767, 4941), // &m -> &ms + new Transition (4777, 4778), // &malte -> &maltes + new Transition (4781, 4937), // &M -> &Ms + new Transition (4785, 4787), // &map -> &maps + new Transition (4821, 4822), // &mda -> &mdas + new Transition (4831, 4832), // &mea -> &meas + new Transition (4878, 4879), // &mida -> &midas + new Transition (4891, 4892), // &minu -> &minus + new Transition (4902, 4903), // &Minu -> &Minus + new Transition (4906, 4907), // &MinusPlu -> &MinusPlus + new Transition (4919, 4920), // &mnplu -> &mnplus + new Transition (4925, 4926), // &model -> &models + new Transition (4947, 4948), // &mstpo -> &mstpos + new Transition (4965, 5895), // &n -> &ns + new Transition (4971, 5904), // &N -> &Ns + new Transition (4993, 4994), // &napo -> &napos + new Transition (5006, 5008), // &natural -> &naturals + new Transition (5010, 5011), // &nb -> &nbs + new Transition (5060, 5061), // &nda -> &ndas + new Transition (5064, 5140), // &ne -> &nes + new Transition (5084, 5148), // &Ne -> &Nes + new Transition (5168, 5169), // &NestedLe -> &NestedLes + new Transition (5169, 5170), // &NestedLes -> &NestedLess + new Transition (5172, 5173), // &NestedLessLe -> &NestedLessLes + new Transition (5173, 5174), // &NestedLessLes -> &NestedLessLess + new Transition (5183, 5184), // &nexi -> &nexis + new Transition (5185, 5187), // &nexist -> &nexists + new Transition (5195, 5215), // &ng -> &ngs + new Transition (5198, 5210), // &nge -> &nges + new Transition (5200, 5204), // &ngeq -> &ngeqs + new Transition (5240, 5242), // &ni -> &nis + new Transition (5256, 5328), // &nl -> &nls + new Transition (5270, 5322), // &nle -> &nles + new Transition (5312, 5316), // &nleq -> &nleqs + new Transition (5322, 5324), // &nles -> &nless + new Transition (5434, 5435), // &NotExi -> &NotExis + new Transition (5436, 5437), // &NotExist -> &NotExists + new Transition (5472, 5473), // &NotGreaterLe -> &NotGreaterLes + new Transition (5473, 5474), // &NotGreaterLes -> &NotGreaterLess + new Transition (5529, 5551), // &NotLe -> &NotLes + new Transition (5551, 5552), // &NotLes -> &NotLess + new Transition (5569, 5570), // &NotLessLe -> &NotLessLes + new Transition (5570, 5571), // &NotLessLes -> &NotLessLess + new Transition (5591, 5592), // &NotNe -> &NotNes + new Transition (5612, 5613), // &NotNestedLe -> &NotNestedLes + new Transition (5613, 5614), // &NotNestedLes -> &NotNestedLess + new Transition (5616, 5617), // &NotNestedLessLe -> &NotNestedLessLes + new Transition (5617, 5618), // &NotNestedLessLes -> &NotNestedLessLess + new Transition (5636, 5637), // &NotPrecede -> &NotPrecedes + new Transition (5660, 5661), // &NotRever -> &NotRevers + new Transition (5702, 5703), // &NotSquareSub -> &NotSquareSubs + new Transition (5715, 5716), // &NotSquareSuper -> &NotSquareSupers + new Transition (5727, 5728), // &NotSub -> &NotSubs + new Transition (5742, 5743), // &NotSucceed -> &NotSucceeds + new Transition (5770, 5771), // &NotSuper -> &NotSupers + new Transition (5823, 5831), // &npar -> &npars + new Transition (5942, 5943), // &nsq -> &nsqs + new Transition (5952, 5958), // &nsub -> &nsubs + new Transition (5973, 5979), // &nsup -> &nsups + new Transition (6034, 6040), // &num -> &nums + new Transition (6043, 6107), // &nv -> &nvs + new Transition (6049, 6050), // &nVDa -> &nVDas + new Transition (6054, 6055), // &nVda -> &nVdas + new Transition (6059, 6060), // &nvDa -> &nvDas + new Transition (6064, 6065), // &nvda -> &nvdas + new Transition (6131, 6378), // &O -> &Os + new Transition (6138, 6382), // &o -> &os + new Transition (6139, 6145), // &oa -> &oas + new Transition (6163, 6185), // &od -> &ods + new Transition (6164, 6165), // &oda -> &odas + new Transition (6248, 6249), // &olcro -> &olcros + new Transition (6249, 6250), // &olcros -> &olcross + new Transition (6291, 6292), // &ominu -> &ominus + new Transition (6337, 6338), // &oplu -> &oplus + new Transition (6342, 6368), // &or -> &ors + new Transition (6387, 6388), // &Osla -> &Oslas + new Transition (6392, 6393), // &osla -> &oslas + new Transition (6412, 6413), // &Otime -> &Otimes + new Transition (6416, 6417), // &otime -> &otimes + new Transition (6419, 6420), // &otimesa -> &otimesas + new Transition (6458, 6459), // &OverParenthe -> &OverParenthes + new Transition (6460, 6461), // &OverParenthesi -> &OverParenthesis + new Transition (6463, 6799), // &p -> &ps + new Transition (6465, 6474), // &par -> &pars + new Transition (6482, 6795), // &P -> &Ps + new Transition (6566, 6567), // &plu -> &plus + new Transition (6567, 6599), // &plus -> &pluss + new Transition (6588, 6589), // &Plu -> &Plus + new Transition (6593, 6594), // &PlusMinu -> &PlusMinus + new Transition (6642, 6786), // &pr -> &prs + new Transition (6655, 6721), // &prec -> &precs + new Transition (6676, 6677), // &Precede -> &Precedes + new Transition (6705, 6717), // &precn -> &precns + new Transition (6731, 6733), // &prime -> &primes + new Transition (6735, 6741), // &prn -> &prns + new Transition (6754, 6765), // &prof -> &profs + new Transition (6809, 6810), // &punc -> &puncs + new Transition (6813, 6839), // &Q -> &Qs + new Transition (6817, 6843), // &q -> &qs + new Transition (6855, 6856), // &quaternion -> &quaternions + new Transition (6862, 6863), // &que -> &ques + new Transition (6876, 7542), // &r -> &rs + new Transition (6886, 7548), // &R -> &Rs + new Transition (6932, 6956), // &rarr -> &rarrs + new Transition (6939, 6940), // &rarrbf -> &rarrbfs + new Transition (6944, 6945), // &rarrf -> &rarrfs + new Transition (6983, 6984), // &rational -> &rationals + new Transition (7012, 7015), // &rbrk -> &rbrks + new Transition (7053, 7069), // &rd -> &rds + new Transition (7076, 7087), // &real -> &reals + new Transition (7099, 7100), // &Rever -> &Revers + new Transition (7136, 7137), // &rfi -> &rfis + new Transition (7199, 7431), // &ri -> &ris + new Transition (7202, 7326), // &right -> &rights + new Transition (7302, 7303), // &rightleftarrow -> &rightleftarrows + new Transition (7311, 7312), // &rightleftharpoon -> &rightleftharpoons + new Transition (7323, 7324), // &rightrightarrow -> &rightrightarrows + new Transition (7362, 7363), // &rightthreetime -> &rightthreetimes + new Transition (7437, 7438), // &risingdot -> &risingdots + new Transition (7455, 7456), // &rmou -> &rmous + new Transition (7492, 7493), // &roplu -> &roplus + new Transition (7498, 7499), // &rotime -> &rotimes + new Transition (7509, 7510), // &RoundImplie -> &RoundImplies + new Transition (7575, 7576), // &rtime -> &rtimes + new Transition (7610, 8073), // &S -> &Ss + new Transition (7617, 8077), // &s -> &ss + new Transition (7631, 7687), // &sc -> &scs + new Transition (7670, 7676), // &scn -> &scns + new Transition (7703, 7724), // &se -> &ses + new Transition (7733, 7734), // &setminu -> &setminus + new Transition (7870, 7871), // &simplu -> &simplus + new Transition (7895, 7907), // &sma -> &smas + new Transition (7897, 7898), // &small -> &smalls + new Transition (7904, 7905), // &smallsetminu -> &smallsetminus + new Transition (7914, 7915), // &smepar -> &smepars + new Transition (7926, 7928), // &smte -> &smtes + new Transition (7959, 7960), // &spade -> &spades + new Transition (7968, 7984), // &sq -> &sqs + new Transition (7971, 7973), // &sqcap -> &sqcaps + new Transition (7976, 7978), // &sqcup -> &sqcups + new Transition (7986, 7990), // &sqsub -> &sqsubs + new Transition (7997, 8001), // &sqsup -> &sqsups + new Transition (8023, 8024), // &SquareInter -> &SquareInters + new Transition (8034, 8035), // &SquareSub -> &SquareSubs + new Transition (8047, 8048), // &SquareSuper -> &SquareSupers + new Transition (8113, 8114), // &straightep -> &straighteps + new Transition (8124, 8125), // &strn -> &strns + new Transition (8128, 8165), // &Sub -> &Subs + new Transition (8131, 8169), // &sub -> &subs + new Transition (8157, 8158), // &subplu -> &subplus + new Transition (8199, 8265), // &succ -> &succs + new Transition (8220, 8221), // &Succeed -> &Succeeds + new Transition (8249, 8261), // &succn -> &succns + new Transition (8282, 8348), // &Sup -> &Sups + new Transition (8284, 8352), // &sup -> &sups + new Transition (8292, 8296), // &supd -> &supds + new Transition (8309, 8310), // &Super -> &Supers + new Transition (8320, 8321), // &suph -> &suphs + new Transition (8345, 8346), // &supplu -> &supplus + new Transition (8400, 8705), // &T -> &Ts + new Transition (8404, 8709), // &t -> &ts + new Transition (8485, 8487), // &theta -> &thetas + new Transition (8495, 8503), // &thick -> &thicks + new Transition (8516, 8517), // &thin -> &thins + new Transition (8527, 8531), // &thk -> &thks + new Transition (8577, 8578), // &time -> × + new Transition (8590, 8614), // &to -> &tos + new Transition (8633, 8690), // &tri -> &tris + new Transition (8673, 8674), // &triminu -> &triminus + new Transition (8687, 8688), // &triplu -> &triplus + new Transition (8768, 9153), // &U -> &Us + new Transition (8775, 9157), // &u -> &us + new Transition (8850, 8851), // &ufi -> &ufis + new Transition (8940, 8941), // &UnderParenthe -> &UnderParenthes + new Transition (8942, 8943), // &UnderParenthesi -> &UnderParenthesis + new Transition (8951, 8952), // &UnionPlu -> &UnionPlus + new Transition (8970, 9092), // &Up -> &Ups + new Transition (8983, 9095), // &up -> &ups + new Transition (9065, 9066), // &uplu -> &uplus + new Transition (9124, 9125), // &upuparrow -> &upuparrows + new Transition (9201, 9454), // &v -> &vs + new Transition (9208, 9252), // &var -> &vars + new Transition (9210, 9211), // &varep -> &vareps + new Transition (9259, 9260), // &varsub -> &varsubs + new Transition (9269, 9270), // &varsup -> &varsups + new Transition (9303, 9450), // &V -> &Vs + new Transition (9321, 9322), // &VDa -> &VDas + new Transition (9326, 9327), // &Vda -> &Vdas + new Transition (9331, 9332), // &vDa -> &vDas + new Transition (9336, 9337), // &vda -> &vdas + new Transition (9425, 9426), // &vn -> &vns + new Transition (9473, 9474), // &Vvda -> &Vvdas + new Transition (9484, 9540), // &W -> &Ws + new Transition (9490, 9544), // &w -> &ws + new Transition (9548, 9636), // &x -> &xs + new Transition (9565, 9632), // &X -> &Xs + new Transition (9599, 9600), // &xni -> &xnis + new Transition (9615, 9616), // &xoplu -> &xoplus + new Transition (9648, 9649), // &xuplu -> &xuplus + new Transition (9665, 9724), // &Y -> &Ys + new Transition (9672, 9728), // &y -> &ys + new Transition (9747, 9840), // &Z -> &Zs + new Transition (9754, 9844) // &z -> &zs + }; + TransitionTable_t = new Transition[499] { + new Transition (0, 8404), // & -> &t + new Transition (1, 269), // &A -> &At + new Transition (4, 5), // &Aacu -> &Aacut + new Transition (8, 275), // &a -> &at + new Transition (11, 12), // &aacu -> &aacut + new Transition (42, 43), // &acu -> &acut + new Transition (164, 165), // &angr -> &angrt + new Transition (172, 176), // &angs -> &angst + new Transition (223, 224), // &ApplyFunc -> &ApplyFunct + new Transition (251, 260), // &as -> &ast + new Transition (294, 295), // &awconin -> &awconint + new Transition (298, 299), // &awin -> &awint + new Transition (362, 364), // &bbrk -> &bbrkt + new Transition (384, 426), // &be -> &bet + new Transition (390, 423), // &Be -> &Bet + new Transition (400, 401), // &bemp -> &bempt + new Transition (443, 481), // &big -> &bigt + new Transition (455, 465), // &bigo -> &bigot + new Transition (457, 458), // &bigodo -> &bigodot + new Transition (471, 477), // &bigs -> &bigst + new Transition (522, 538), // &black -> &blackt + new Transition (554, 555), // &blacktrianglelef -> &blacktriangleleft + new Transition (560, 561), // &blacktrianglerigh -> &blacktriangleright + new Transition (588, 589), // &bNo -> &bNot + new Transition (591, 592), // &bno -> &bnot + new Transition (598, 602), // &bo -> &bot + new Transition (602, 604), // &bot -> &bott + new Transition (608, 609), // &bow -> &bowt + new Transition (613, 667), // &box -> &boxt + new Transition (771, 772), // &bulle -> &bullet + new Transition (792, 793), // &Cacu -> &Cacut + new Transition (796, 1287), // &c -> &ct + new Transition (799, 800), // &cacu -> &cacut + new Transition (825, 826), // &capdo -> &capdot + new Transition (828, 829), // &Capi -> &Capit + new Transition (839, 840), // &CapitalDifferen -> &CapitalDifferent + new Transition (849, 850), // &care -> &caret + new Transition (897, 898), // &Cconin -> &Cconint + new Transition (908, 909), // &Cdo -> &Cdot + new Transition (912, 913), // &cdo -> &cdot + new Transition (928, 929), // &cemp -> &cempt + new Transition (933, 934), // &cen -> ¢ + new Transition (936, 937), // &Cen -> &Cent + new Transition (941, 942), // &CenterDo -> &CenterDot + new Transition (947, 948), // ¢erdo -> ¢erdot + new Transition (995, 996), // &circlearrowlef -> &circlearrowleft + new Transition (1001, 1002), // &circlearrowrigh -> &circlearrowright + new Transition (1006, 1007), // &circledas -> &circledast + new Transition (1025, 1026), // &CircleDo -> &CircleDot + new Transition (1056, 1057), // &cirfnin -> &cirfnint + new Transition (1078, 1079), // &ClockwiseCon -> &ClockwiseCont + new Transition (1084, 1085), // &ClockwiseContourIn -> &ClockwiseContourInt + new Transition (1107, 1108), // &CloseCurlyDoubleQuo -> &CloseCurlyDoubleQuot + new Transition (1113, 1114), // &CloseCurlyQuo -> &CloseCurlyQuot + new Transition (1123, 1124), // &clubsui -> &clubsuit + new Transition (1144, 1146), // &comma -> &commat + new Transition (1157, 1158), // &complemen -> &complement + new Transition (1168, 1169), // &congdo -> &congdot + new Transition (1171, 1187), // &Con -> &Cont + new Transition (1176, 1177), // &Congruen -> &Congruent + new Transition (1180, 1181), // &Conin -> &Conint + new Transition (1184, 1185), // &conin -> &conint + new Transition (1192, 1193), // &ContourIn -> &ContourInt + new Transition (1214, 1215), // &Coproduc -> &Coproduct + new Transition (1227, 1228), // &Coun -> &Count + new Transition (1242, 1243), // &CounterClockwiseCon -> &CounterClockwiseCont + new Transition (1248, 1249), // &CounterClockwiseContourIn -> &CounterClockwiseContourInt + new Transition (1289, 1290), // &ctdo -> &ctdot + new Transition (1338, 1339), // &cupdo -> &cupdot + new Transition (1390, 1391), // &curvearrowlef -> &curvearrowleft + new Transition (1396, 1397), // &curvearrowrigh -> &curvearrowright + new Transition (1412, 1413), // &cwconin -> &cwconint + new Transition (1416, 1417), // &cwin -> &cwint + new Transition (1421, 1422), // &cylc -> &cylct + new Transition (1432, 2067), // &d -> &dt + new Transition (1440, 1441), // &dale -> &dalet + new Transition (1503, 1504), // &DDo -> &DDot + new Transition (1510, 1511), // &ddo -> &ddot + new Transition (1520, 1522), // &Del -> &Delt + new Transition (1525, 1526), // &del -> &delt + new Transition (1530, 1531), // &demp -> &dempt + new Transition (1538, 1539), // &dfish -> &dfisht + new Transition (1561, 1562), // &Diacri -> &Diacrit + new Transition (1569, 1570), // &DiacriticalAcu -> &DiacriticalAcut + new Transition (1574, 1575), // &DiacriticalDo -> &DiacriticalDot + new Transition (1583, 1584), // &DiacriticalDoubleAcu -> &DiacriticalDoubleAcut + new Transition (1614, 1615), // &diamondsui -> &diamondsuit + new Transition (1626, 1627), // &Differen -> &Different + new Transition (1650, 1651), // ÷on -> ÷ont + new Transition (1679, 1694), // &do -> &dot + new Transition (1685, 1692), // &Do -> &Dot + new Transition (1697, 1698), // &DotDo -> &DotDot + new Transition (1704, 1705), // &doteqdo -> &doteqdot + new Transition (1750, 1751), // &DoubleCon -> &DoubleCont + new Transition (1756, 1757), // &DoubleContourIn -> &DoubleContourInt + new Transition (1765, 1766), // &DoubleDo -> &DoubleDot + new Transition (1778, 1779), // &DoubleLef -> &DoubleLeft + new Transition (1789, 1790), // &DoubleLeftRigh -> &DoubleLeftRight + new Transition (1806, 1807), // &DoubleLongLef -> &DoubleLongLeft + new Transition (1817, 1818), // &DoubleLongLeftRigh -> &DoubleLongLeftRight + new Transition (1828, 1829), // &DoubleLongRigh -> &DoubleLongRight + new Transition (1839, 1840), // &DoubleRigh -> &DoubleRight + new Transition (1871, 1872), // &DoubleVer -> &DoubleVert + new Transition (1941, 1942), // &downharpoonlef -> &downharpoonleft + new Transition (1947, 1948), // &downharpoonrigh -> &downharpoonright + new Transition (1952, 1953), // &DownLef -> &DownLeft + new Transition (1957, 1958), // &DownLeftRigh -> &DownLeftRight + new Transition (1961, 1962), // &DownLeftRightVec -> &DownLeftRightVect + new Transition (1971, 1972), // &DownLeftTeeVec -> &DownLeftTeeVect + new Transition (1978, 1979), // &DownLeftVec -> &DownLeftVect + new Transition (1990, 1991), // &DownRigh -> &DownRight + new Transition (1997, 1998), // &DownRightTeeVec -> &DownRightTeeVect + new Transition (2004, 2005), // &DownRightVec -> &DownRightVect + new Transition (2040, 2057), // &Ds -> &Dst + new Transition (2044, 2062), // &ds -> &dst + new Transition (2069, 2070), // &dtdo -> &dtdot + new Transition (2108, 2436), // &E -> &Et + new Transition (2111, 2112), // &Eacu -> &Eacut + new Transition (2115, 2439), // &e -> &et + new Transition (2118, 2119), // &eacu -> &eacut + new Transition (2122, 2123), // &eas -> &east + new Transition (2159, 2160), // &eDDo -> &eDDot + new Transition (2163, 2164), // &Edo -> &Edot + new Transition (2166, 2167), // &eDo -> &eDot + new Transition (2170, 2171), // &edo -> &edot + new Transition (2177, 2178), // &efDo -> &efDot + new Transition (2201, 2202), // &egsdo -> &egsdot + new Transition (2210, 2211), // &Elemen -> &Element + new Transition (2214, 2215), // &elin -> &elint + new Transition (2225, 2226), // &elsdo -> &elsdot + new Transition (2238, 2239), // &emp -> &empt + new Transition (2243, 2244), // &emptyse -> &emptyset + new Transition (2246, 2247), // &Emp -> &Empt + new Transition (2356, 2357), // &eqslan -> &eqslant + new Transition (2358, 2359), // &eqslantg -> &eqslantgt + new Transition (2384, 2385), // &eques -> &equest + new Transition (2415, 2416), // &erDo -> &erDot + new Transition (2427, 2428), // &esdo -> &esdot + new Transition (2463, 2464), // &exis -> &exist + new Transition (2468, 2469), // &Exis -> &Exist + new Transition (2474, 2475), // &expec -> &expect + new Transition (2476, 2477), // &expecta -> &expectat + new Transition (2486, 2487), // &Exponen -> &Exponent + new Transition (2496, 2497), // &exponen -> &exponent + new Transition (2511, 2512), // &fallingdo -> &fallingdot + new Transition (2592, 2600), // &fl -> &flt + new Transition (2593, 2594), // &fla -> &flat + new Transition (2634, 2635), // &Fourier -> &Fouriert + new Transition (2641, 2642), // &fpar -> &fpart + new Transition (2644, 2645), // &fpartin -> &fpartint + new Transition (2701, 2942), // &g -> > + new Transition (2704, 2705), // &gacu -> &gacut + new Transition (2708, 2940), // &G -> &Gt + new Transition (2756, 2757), // &Gdo -> &Gdot + new Transition (2760, 2761), // &gdo -> &gdot + new Transition (2778, 2779), // &geqslan -> &geqslant + new Transition (2787, 2788), // &gesdo -> &gesdot + new Transition (2868, 2869), // &Grea -> &Great + new Transition (2896, 2897), // &GreaterGrea -> &GreaterGreat + new Transition (2909, 2910), // &GreaterSlan -> &GreaterSlant + new Transition (2951, 2952), // >do -> >dot + new Transition (2962, 2963), // >ques -> >quest + new Transition (2977, 2978), // >rdo -> >rdot + new Transition (3004, 3005), // &gver -> &gvert + new Transition (3015, 3058), // &Ha -> &Hat + new Transition (3032, 3033), // &hamil -> &hamilt + new Transition (3076, 3077), // &hear -> &heart + new Transition (3081, 3082), // &heartsui -> &heartsuit + new Transition (3104, 3105), // &Hilber -> &Hilbert + new Transition (3131, 3132), // &hom -> &homt + new Transition (3133, 3134), // &homth -> &homtht + new Transition (3140, 3141), // &hooklef -> &hookleft + new Transition (3151, 3152), // &hookrigh -> &hookright + new Transition (3175, 3176), // &Horizon -> &Horizont + new Transition (3184, 3197), // &Hs -> &Hst + new Transition (3188, 3202), // &hs -> &hst + new Transition (3236, 3528), // &I -> &It + new Transition (3239, 3240), // &Iacu -> &Iacut + new Transition (3243, 3526), // &i -> &it + new Transition (3246, 3247), // &iacu -> &iacut + new Transition (3266, 3267), // &Ido -> &Idot + new Transition (3305, 3306), // &iiiin -> &iiiint + new Transition (3308, 3309), // &iiin -> &iiint + new Transition (3316, 3317), // &iio -> &iiot + new Transition (3337, 3362), // &ima -> &imat + new Transition (3359, 3360), // &imagpar -> &imagpart + new Transition (3378, 3401), // &in -> &int + new Transition (3387, 3389), // &infin -> &infint + new Transition (3395, 3396), // &inodo -> &inodot + new Transition (3398, 3399), // &In -> &Int + new Transition (3427, 3428), // &Intersec -> &Intersect + new Transition (3467, 3489), // &io -> &iot + new Transition (3471, 3486), // &Io -> &Iot + new Transition (3500, 3501), // &iques -> ¿ + new Transition (3515, 3516), // &isindo -> &isindot + new Transition (3578, 3579), // &jma -> &jmat + new Transition (3692, 4698), // &l -> < + new Transition (3693, 3794), // &lA -> &lAt + new Transition (3698, 4696), // &L -> &Lt + new Transition (3701, 3702), // &Lacu -> &Lacut + new Transition (3705, 3792), // &la -> &lat + new Transition (3707, 3708), // &lacu -> &lacut + new Transition (3713, 3714), // &laemp -> &laempt + new Transition (3750, 3751), // &Laplace -> &Laplacet + new Transition (3766, 3789), // &larr -> &larrt + new Transition (3899, 3900), // &Lef -> &Left + new Transition (3911, 3912), // &LeftAngleBracke -> &LeftAngleBracket + new Transition (3925, 3926), // &lef -> &left + new Transition (3926, 4109), // &left -> &leftt + new Transition (3931, 3948), // &leftarrow -> &leftarrowt + new Transition (3940, 3941), // &LeftArrowRigh -> &LeftArrowRight + new Transition (3972, 3973), // &LeftDoubleBracke -> &LeftDoubleBracket + new Transition (3982, 3983), // &LeftDownTeeVec -> &LeftDownTeeVect + new Transition (3989, 3990), // &LeftDownVec -> &LeftDownVect + new Transition (4021, 4022), // &leftlef -> &leftleft + new Transition (4033, 4034), // &LeftRigh -> &LeftRight + new Transition (4044, 4045), // &Leftrigh -> &Leftright + new Transition (4055, 4056), // &leftrigh -> &leftright + new Transition (4087, 4088), // &LeftRightVec -> &LeftRightVect + new Transition (4104, 4105), // &LeftTeeVec -> &LeftTeeVect + new Transition (4113, 4114), // &leftthree -> &leftthreet + new Transition (4146, 4147), // &LeftUpDownVec -> &LeftUpDownVect + new Transition (4156, 4157), // &LeftUpTeeVec -> &LeftUpTeeVect + new Transition (4163, 4164), // &LeftUpVec -> &LeftUpVect + new Transition (4174, 4175), // &LeftVec -> &LeftVect + new Transition (4194, 4195), // &leqslan -> &leqslant + new Transition (4203, 4204), // &lesdo -> &lesdot + new Transition (4224, 4225), // &lessdo -> &lessdot + new Transition (4229, 4230), // &lesseqg -> &lesseqgt + new Transition (4234, 4235), // &lesseqqg -> &lesseqqgt + new Transition (4248, 4249), // &LessEqualGrea -> &LessEqualGreat + new Transition (4266, 4267), // &LessGrea -> &LessGreat + new Transition (4271, 4272), // &lessg -> &lessgt + new Transition (4287, 4288), // &LessSlan -> &LessSlant + new Transition (4304, 4305), // &lfish -> &lfisht + new Transition (4348, 4375), // &ll -> &llt + new Transition (4362, 4363), // &Llef -> &Lleft + new Transition (4382, 4383), // &Lmido -> &Lmidot + new Transition (4388, 4389), // &lmido -> &lmidot + new Transition (4393, 4394), // &lmous -> &lmoust + new Transition (4422, 4573), // &lo -> &lot + new Transition (4439, 4440), // &LongLef -> &LongLeft + new Transition (4449, 4450), // &Longlef -> &Longleft + new Transition (4461, 4462), // &longlef -> &longleft + new Transition (4472, 4473), // &LongLeftRigh -> &LongLeftRight + new Transition (4483, 4484), // &Longleftrigh -> &Longleftright + new Transition (4494, 4495), // &longleftrigh -> &longleftright + new Transition (4505, 4506), // &longmaps -> &longmapst + new Transition (4512, 4513), // &LongRigh -> &LongRight + new Transition (4523, 4524), // &Longrigh -> &Longright + new Transition (4534, 4535), // &longrigh -> &longright + new Transition (4551, 4552), // &looparrowlef -> &looparrowleft + new Transition (4557, 4558), // &looparrowrigh -> &looparrowright + new Transition (4581, 4582), // &lowas -> &lowast + new Transition (4593, 4594), // &LowerLef -> &LowerLeft + new Transition (4604, 4605), // &LowerRigh -> &LowerRight + new Transition (4625, 4626), // &lparl -> &lparlt + new Transition (4628, 4648), // &lr -> &lrt + new Transition (4652, 4689), // &ls -> &lst + new Transition (4658, 4684), // &Ls -> &Lst + new Transition (4707, 4708), // <do -> <dot + new Transition (4728, 4729), // <ques -> <quest + new Transition (4757, 4758), // &lver -> &lvert + new Transition (4772, 4775), // &mal -> &malt + new Transition (4787, 4788), // &maps -> &mapst + new Transition (4798, 4799), // &mapstolef -> &mapstoleft + new Transition (4827, 4828), // &mDDo -> &mDDot + new Transition (4857, 4858), // &Mellin -> &Mellint + new Transition (4879, 4880), // &midas -> &midast + new Transition (4887, 4888), // &middo -> · + new Transition (4941, 4945), // &ms -> &mst + new Transition (4954, 4955), // &mul -> &mult + new Transition (4965, 5988), // &n -> &nt + new Transition (4966, 5001), // &na -> &nat + new Transition (4971, 5992), // &N -> &Nt + new Transition (4974, 4975), // &Nacu -> &Nacut + new Transition (4979, 4980), // &nacu -> &nacut + new Transition (5049, 5050), // &ncongdo -> &ncongdot + new Transition (5081, 5082), // &nedo -> &nedot + new Transition (5086, 5087), // &Nega -> &Negat + new Transition (5148, 5149), // &Nes -> &Nest + new Transition (5155, 5156), // &NestedGrea -> &NestedGreat + new Transition (5162, 5163), // &NestedGreaterGrea -> &NestedGreaterGreat + new Transition (5184, 5185), // &nexis -> &nexist + new Transition (5195, 5221), // &ng -> &ngt + new Transition (5207, 5208), // &ngeqslan -> &ngeqslant + new Transition (5212, 5219), // &nG -> &nGt + new Transition (5256, 5334), // &nl -> &nlt + new Transition (5272, 5332), // &nL -> &nLt + new Transition (5274, 5275), // &nLef -> &nLeft + new Transition (5282, 5283), // &nlef -> &nleft + new Transition (5293, 5294), // &nLeftrigh -> &nLeftright + new Transition (5304, 5305), // &nleftrigh -> &nleftright + new Transition (5319, 5320), // &nleqslan -> &nleqslant + new Transition (5347, 5376), // &No -> &Not + new Transition (5372, 5378), // &no -> ¬ + new Transition (5387, 5388), // &NotCongruen -> &NotCongruent + new Transition (5404, 5405), // &NotDoubleVer -> &NotDoubleVert + new Transition (5419, 5420), // &NotElemen -> &NotElement + new Transition (5435, 5436), // &NotExis -> &NotExist + new Transition (5442, 5443), // &NotGrea -> &NotGreat + new Transition (5466, 5467), // &NotGreaterGrea -> &NotGreaterGreat + new Transition (5479, 5480), // &NotGreaterSlan -> &NotGreaterSlant + new Transition (5516, 5517), // ¬indo -> ¬indot + new Transition (5530, 5531), // &NotLef -> &NotLeft + new Transition (5563, 5564), // &NotLessGrea -> &NotLessGreat + new Transition (5576, 5577), // &NotLessSlan -> &NotLessSlant + new Transition (5592, 5593), // &NotNes -> &NotNest + new Transition (5599, 5600), // &NotNestedGrea -> &NotNestedGreat + new Transition (5606, 5607), // &NotNestedGreaterGrea -> &NotNestedGreaterGreat + new Transition (5648, 5649), // &NotPrecedesSlan -> &NotPrecedesSlant + new Transition (5668, 5669), // &NotReverseElemen -> &NotReverseElement + new Transition (5673, 5674), // &NotRigh -> &NotRight + new Transition (5704, 5705), // &NotSquareSubse -> &NotSquareSubset + new Transition (5717, 5718), // &NotSquareSuperse -> &NotSquareSuperset + new Transition (5729, 5730), // &NotSubse -> &NotSubset + new Transition (5754, 5755), // &NotSucceedsSlan -> &NotSucceedsSlant + new Transition (5772, 5773), // &NotSuperse -> &NotSuperset + new Transition (5811, 5812), // &NotVer -> &NotVert + new Transition (5823, 5834), // &npar -> &npart + new Transition (5839, 5840), // &npolin -> &npolint + new Transition (5855, 5889), // &nr -> &nrt + new Transition (5871, 5872), // &nRigh -> &nRight + new Transition (5881, 5882), // &nrigh -> &nright + new Transition (5912, 5913), // &nshor -> &nshort + new Transition (5959, 5960), // &nsubse -> &nsubset + new Transition (5980, 5981), // &nsupse -> &nsupset + new Transition (6015, 6016), // &ntrianglelef -> &ntriangleleft + new Transition (6024, 6025), // &ntrianglerigh -> &ntriangleright + new Transition (6068, 6071), // &nvg -> &nvgt + new Transition (6084, 6091), // &nvl -> &nvlt + new Transition (6097, 6102), // &nvr -> &nvrt + new Transition (6131, 6399), // &O -> &Ot + new Transition (6134, 6135), // &Oacu -> &Oacut + new Transition (6138, 6405), // &o -> &ot + new Transition (6141, 6142), // &oacu -> &oacut + new Transition (6145, 6146), // &oas -> &oast + new Transition (6182, 6183), // &odo -> &odot + new Transition (6210, 6225), // &og -> &ogt + new Transition (6235, 6236), // &oin -> &oint + new Transition (6238, 6256), // &ol -> &olt + new Transition (6322, 6323), // &OpenCurlyDoubleQuo -> &OpenCurlyDoubleQuot + new Transition (6328, 6329), // &OpenCurlyQuo -> &OpenCurlyQuot + new Transition (6448, 6449), // &OverBracke -> &OverBracket + new Transition (6455, 6456), // &OverParen -> &OverParent + new Transition (6465, 6480), // &par -> &part + new Transition (6484, 6485), // &Par -> &Part + new Transition (6498, 6513), // &per -> &pert + new Transition (6500, 6501), // &percn -> &percnt + new Transition (6534, 6535), // &phmma -> &phmmat + new Transition (6543, 6545), // &pi -> &pit + new Transition (6567, 6603), // &plus -> &plust + new Transition (6624, 6625), // &poin -> &point + new Transition (6627, 6628), // &pointin -> &pointint + new Transition (6688, 6689), // &PrecedesSlan -> &PrecedesSlant + new Transition (6751, 6752), // &Produc -> &Product + new Transition (6770, 6783), // &prop -> &propt + new Transition (6774, 6775), // &Propor -> &Proport + new Transition (6822, 6823), // &qin -> &qint + new Transition (6848, 6849), // &qua -> &quat + new Transition (6859, 6860), // &quatin -> &quatint + new Transition (6863, 6864), // &ques -> &quest + new Transition (6873, 6874), // &quo -> " + new Transition (6876, 7567), // &r -> &rt + new Transition (6877, 6968), // &rA -> &rAt + new Transition (6882, 6973), // &ra -> &rat + new Transition (6889, 6890), // &Racu -> &Racut + new Transition (6893, 6894), // &racu -> &racut + new Transition (6903, 6904), // &raemp -> &raempt + new Transition (6926, 6960), // &Rarr -> &Rarrt + new Transition (6932, 6963), // &rarr -> &rarrt + new Transition (7084, 7085), // &realpar -> &realpart + new Transition (7089, 7090), // &rec -> &rect + new Transition (7107, 7108), // &ReverseElemen -> &ReverseElement + new Transition (7138, 7139), // &rfish -> &rfisht + new Transition (7173, 7174), // &Righ -> &Right + new Transition (7185, 7186), // &RightAngleBracke -> &RightAngleBracket + new Transition (7201, 7202), // &righ -> &right + new Transition (7202, 7354), // &right -> &rightt + new Transition (7207, 7223), // &rightarrow -> &rightarrowt + new Transition (7215, 7216), // &RightArrowLef -> &RightArrowLeft + new Transition (7247, 7248), // &RightDoubleBracke -> &RightDoubleBracket + new Transition (7257, 7258), // &RightDownTeeVec -> &RightDownTeeVect + new Transition (7264, 7265), // &RightDownVec -> &RightDownVect + new Transition (7296, 7297), // &rightlef -> &rightleft + new Transition (7317, 7318), // &rightrigh -> &rightright + new Transition (7349, 7350), // &RightTeeVec -> &RightTeeVect + new Transition (7358, 7359), // &rightthree -> &rightthreet + new Transition (7391, 7392), // &RightUpDownVec -> &RightUpDownVect + new Transition (7401, 7402), // &RightUpTeeVec -> &RightUpTeeVect + new Transition (7408, 7409), // &RightUpVec -> &RightUpVect + new Transition (7419, 7420), // &RightVec -> &RightVect + new Transition (7436, 7437), // &risingdo -> &risingdot + new Transition (7456, 7457), // &rmous -> &rmoust + new Transition (7469, 7495), // &ro -> &rot + new Transition (7516, 7517), // &rparg -> &rpargt + new Transition (7523, 7524), // &rppolin -> &rppolint + new Transition (7534, 7535), // &Rrigh -> &Rright + new Transition (7585, 7586), // &rtril -> &rtrilt + new Transition (7610, 8096), // &S -> &St + new Transition (7613, 7614), // &Sacu -> &Sacut + new Transition (7617, 8100), // &s -> &st + new Transition (7620, 7621), // &sacu -> &sacut + new Transition (7684, 7685), // &scpolin -> &scpolint + new Transition (7696, 7697), // &sdo -> &sdot + new Transition (7703, 7729), // &se -> &set + new Transition (7718, 7719), // &sec -> § + new Transition (7738, 7739), // &sex -> &sext + new Transition (7774, 7775), // &Shor -> &Short + new Transition (7788, 7789), // &ShortLef -> &ShortLeft + new Transition (7797, 7798), // &shor -> &short + new Transition (7815, 7816), // &ShortRigh -> &ShortRight + new Transition (7850, 7851), // &simdo -> &simdot + new Transition (7894, 7924), // &sm -> &smt + new Transition (7899, 7900), // &smallse -> &smallset + new Transition (7937, 7938), // &sof -> &soft + new Transition (7963, 7964), // &spadesui -> &spadesuit + new Transition (7981, 7982), // &Sqr -> &Sqrt + new Transition (7991, 7992), // &sqsubse -> &sqsubset + new Transition (8002, 8003), // &sqsupse -> &sqsupset + new Transition (8020, 8021), // &SquareIn -> &SquareInt + new Transition (8026, 8027), // &SquareIntersec -> &SquareIntersect + new Transition (8036, 8037), // &SquareSubse -> &SquareSubset + new Transition (8049, 8050), // &SquareSuperse -> &SquareSuperset + new Transition (8077, 8091), // &ss -> &sst + new Transition (8081, 8082), // &sse -> &sset + new Transition (8110, 8111), // &straigh -> &straight + new Transition (8134, 8135), // &subdo -> &subdot + new Transition (8142, 8143), // &subedo -> &subedot + new Transition (8147, 8148), // &submul -> &submult + new Transition (8166, 8167), // &Subse -> &Subset + new Transition (8170, 8171), // &subse -> &subset + new Transition (8232, 8233), // &SucceedsSlan -> &SucceedsSlant + new Transition (8272, 8273), // &SuchTha -> &SuchThat + new Transition (8293, 8294), // &supdo -> &supdot + new Transition (8305, 8306), // &supedo -> &supedot + new Transition (8311, 8312), // &Superse -> &Superset + new Transition (8335, 8336), // &supmul -> &supmult + new Transition (8349, 8350), // &Supse -> &Supset + new Transition (8353, 8354), // &supse -> &supset + new Transition (8408, 8409), // &targe -> &target + new Transition (8446, 8447), // &tdo -> &tdot + new Transition (8462, 8484), // &the -> &thet + new Transition (8468, 8481), // &The -> &Thet + new Transition (8587, 8588), // &tin -> &tint + new Transition (8597, 8598), // &topbo -> &topbot + new Transition (8633, 8693), // &tri -> &trit + new Transition (8647, 8648), // &trianglelef -> &triangleleft + new Transition (8658, 8659), // &trianglerigh -> &triangleright + new Transition (8665, 8666), // &trido -> &tridot + new Transition (8682, 8683), // &TripleDo -> &TripleDot + new Transition (8705, 8727), // &Ts -> &Tst + new Transition (8709, 8732), // &ts -> &tst + new Transition (8739, 8740), // &twix -> &twixt + new Transition (8749, 8750), // &twoheadlef -> &twoheadleft + new Transition (8760, 8761), // &twoheadrigh -> &twoheadright + new Transition (8768, 9166), // &U -> &Ut + new Transition (8771, 8772), // &Uacu -> &Uacut + new Transition (8775, 9161), // &u -> &ut + new Transition (8778, 8779), // &uacu -> &uacut + new Transition (8852, 8853), // &ufish -> &ufisht + new Transition (8887, 8900), // &ul -> &ult + new Transition (8930, 8931), // &UnderBracke -> &UnderBracket + new Transition (8937, 8938), // &UnderParen -> &UnderParent + new Transition (9055, 9056), // &upharpoonlef -> &upharpoonleft + new Transition (9061, 9062), // &upharpoonrigh -> &upharpoonright + new Transition (9073, 9074), // &UpperLef -> &UpperLeft + new Transition (9084, 9085), // &UpperRigh -> &UpperRight + new Transition (9127, 9149), // &ur -> &urt + new Transition (9163, 9164), // &utdo -> &utdot + new Transition (9205, 9206), // &vangr -> &vangrt + new Transition (9208, 9279), // &var -> &vart + new Transition (9224, 9225), // &varno -> &varnot + new Transition (9239, 9240), // &varprop -> &varpropt + new Transition (9261, 9262), // &varsubse -> &varsubset + new Transition (9271, 9272), // &varsupse -> &varsupset + new Transition (9281, 9282), // &varthe -> &varthet + new Transition (9294, 9295), // &vartrianglelef -> &vartriangleleft + new Transition (9300, 9301), // &vartrianglerigh -> &vartriangleright + new Transition (9360, 9370), // &Ver -> &Vert + new Transition (9365, 9372), // &ver -> &vert + new Transition (9392, 9393), // &VerticalSepara -> &VerticalSeparat + new Transition (9420, 9421), // &vl -> &vlt + new Transition (9445, 9446), // &vr -> &vrt + new Transition (9536, 9537), // &wrea -> &wreat + new Transition (9560, 9561), // &xd -> &xdt + new Transition (9602, 9618), // &xo -> &xot + new Transition (9604, 9605), // &xodo -> &xodot + new Transition (9645, 9651), // &xu -> &xut + new Transition (9668, 9669), // &Yacu -> &Yacut + new Transition (9675, 9676), // &yacu -> &yacut + new Transition (9750, 9751), // &Zacu -> &Zacut + new Transition (9757, 9758), // &zacu -> &zacut + new Transition (9778, 9779), // &Zdo -> &Zdot + new Transition (9782, 9783), // &zdo -> &zdot + new Transition (9785, 9808), // &ze -> &zet + new Transition (9786, 9787), // &zee -> &zeet + new Transition (9791, 9805), // &Ze -> &Zet + new Transition (9796, 9797) // &ZeroWid -> &ZeroWidt + }; + TransitionTable_u = new Transition[278] { + new Transition (0, 8775), // & -> &u + new Transition (1, 281), // &A -> &Au + new Transition (3, 4), // &Aac -> &Aacu + new Transition (8, 285), // &a -> &au + new Transition (10, 11), // &aac -> &aacu + new Transition (27, 42), // &ac -> &acu + new Transition (220, 221), // &ApplyF -> &ApplyFu + new Transition (301, 767), // &b -> &bu + new Transition (331, 781), // &B -> &Bu + new Transition (380, 381), // &bdq -> &bdqu + new Transition (386, 387), // &beca -> &becau + new Transition (392, 393), // &Beca -> &Becau + new Transition (411, 412), // &berno -> &bernou + new Transition (416, 417), // &Berno -> &Bernou + new Transition (443, 497), // &big -> &bigu + new Transition (444, 452), // &bigc -> &bigcu + new Transition (461, 462), // &bigopl -> &bigoplu + new Transition (473, 474), // &bigsqc -> &bigsqcu + new Transition (488, 494), // &bigtriangle -> &bigtriangleu + new Transition (499, 500), // &bigupl -> &biguplu + new Transition (532, 533), // &blacksq -> &blacksqu + new Transition (582, 583), // &bneq -> &bnequ + new Transition (613, 678), // &box -> &boxu + new Transition (636, 650), // &boxH -> &boxHu + new Transition (638, 654), // &boxh -> &boxhu + new Transition (658, 659), // &boxmin -> &boxminu + new Transition (663, 664), // &boxpl -> &boxplu + new Transition (763, 764), // &bsolhs -> &bsolhsu + new Transition (789, 1315), // &C -> &Cu + new Transition (791, 792), // &Cac -> &Cacu + new Transition (796, 1292), // &c -> &cu + new Transition (798, 799), // &cac -> &cacu + new Transition (813, 814), // &capbrc -> &capbrcu + new Transition (817, 821), // &capc -> &capcu + new Transition (861, 900), // &cc -> &ccu + new Transition (1034, 1035), // &CircleMin -> &CircleMinu + new Transition (1039, 1040), // &CirclePl -> &CirclePlu + new Transition (1080, 1081), // &ClockwiseConto -> &ClockwiseContou + new Transition (1094, 1095), // &CloseC -> &CloseCu + new Transition (1100, 1101), // &CloseCurlyDo -> &CloseCurlyDou + new Transition (1105, 1106), // &CloseCurlyDoubleQ -> &CloseCurlyDoubleQu + new Transition (1111, 1112), // &CloseCurlyQ -> &CloseCurlyQu + new Transition (1117, 1118), // &cl -> &clu + new Transition (1120, 1122), // &clubs -> &clubsu + new Transition (1126, 1226), // &Co -> &Cou + new Transition (1173, 1174), // &Congr -> &Congru + new Transition (1188, 1189), // &Conto -> &Contou + new Transition (1212, 1213), // &Coprod -> &Coprodu + new Transition (1244, 1245), // &CounterClockwiseConto -> &CounterClockwiseContou + new Transition (1274, 1278), // &cs -> &csu + new Transition (1330, 1334), // &cupc -> &cupcu + new Transition (1362, 1363), // &curlyeqs -> &curlyeqsu + new Transition (1432, 2077), // &d -> &du + new Transition (1568, 1569), // &DiacriticalAc -> &DiacriticalAcu + new Transition (1574, 1577), // &DiacriticalDo -> &DiacriticalDou + new Transition (1582, 1583), // &DiacriticalDoubleAc -> &DiacriticalDoubleAcu + new Transition (1612, 1613), // &diamonds -> &diamondsu + new Transition (1679, 1731), // &do -> &dou + new Transition (1685, 1744), // &Do -> &Dou + new Transition (1708, 1709), // &DotEq -> &DotEqu + new Transition (1715, 1716), // &dotmin -> &dotminu + new Transition (1720, 1721), // &dotpl -> &dotplu + new Transition (1725, 1726), // &dotsq -> &dotsqu + new Transition (1752, 1753), // &DoubleConto -> &DoubleContou + new Transition (2108, 2447), // &E -> &Eu + new Transition (2110, 2111), // &Eac -> &Eacu + new Transition (2115, 2451), // &e -> &eu + new Transition (2117, 2118), // &eac -> &eacu + new Transition (2255, 2256), // &EmptySmallSq -> &EmptySmallSqu + new Transition (2273, 2274), // &EmptyVerySmallSq -> &EmptyVerySmallSqu + new Transition (2319, 2320), // &epl -> &eplu + new Transition (2339, 2372), // &eq -> &equ + new Transition (2367, 2368), // &Eq -> &Equ + new Transition (2392, 2393), // &Equilibri -> &Equilibriu + new Transition (2565, 2566), // &FilledSmallSq -> &FilledSmallSqu + new Transition (2581, 2582), // &FilledVerySmallSq -> &FilledVerySmallSqu + new Transition (2608, 2630), // &Fo -> &Fou + new Transition (2703, 2704), // &gac -> &gacu + new Transition (2873, 2874), // &GreaterEq -> &GreaterEqu + new Transition (2883, 2884), // &GreaterF -> &GreaterFu + new Transition (2888, 2889), // &GreaterFullEq -> &GreaterFullEqu + new Transition (2912, 2913), // &GreaterSlantEq -> &GreaterSlantEqu + new Transition (2959, 2960), // >q -> >qu + new Transition (3014, 3207), // &H -> &Hu + new Transition (3078, 3080), // &hearts -> &heartsu + new Transition (3214, 3215), // &HumpDownH -> &HumpDownHu + new Transition (3220, 3221), // &HumpEq -> &HumpEqu + new Transition (3226, 3227), // &hyb -> &hybu + new Transition (3236, 3539), // &I -> &Iu + new Transition (3238, 3239), // &Iac -> &Iacu + new Transition (3243, 3544), // &i -> &iu + new Transition (3245, 3246), // &iac -> &iacu + new Transition (3497, 3498), // &iq -> &iqu + new Transition (3555, 3608), // &J -> &Ju + new Transition (3561, 3613), // &j -> &ju + new Transition (3692, 4742), // &l -> &lu + new Transition (3700, 3701), // &Lac -> &Lacu + new Transition (3706, 3707), // &lac -> &lacu + new Transition (3755, 3756), // &laq -> &laqu + new Transition (3832, 3835), // &lbrksl -> &lbrkslu + new Transition (3843, 3862), // &lc -> &lcu + new Transition (3873, 3874), // &ldq -> &ldqu + new Transition (3879, 3885), // &ldr -> &ldru + new Transition (3962, 3963), // &LeftDo -> &LeftDou + new Transition (4010, 4016), // &leftharpoon -> &leftharpoonu + new Transition (4075, 4076), // &leftrightsq -> &leftrightsqu + new Transition (4133, 4134), // &LeftTriangleEq -> &LeftTriangleEqu + new Transition (4241, 4242), // &LessEq -> &LessEqu + new Transition (4253, 4254), // &LessF -> &LessFu + new Transition (4258, 4259), // &LessFullEq -> &LessFullEqu + new Transition (4290, 4291), // &LessSlantEq -> &LessSlantEqu + new Transition (4327, 4330), // &lhar -> &lharu + new Transition (4391, 4392), // &lmo -> &lmou + new Transition (4569, 4570), // &lopl -> &loplu + new Transition (4654, 4655), // &lsaq -> &lsaqu + new Transition (4676, 4679), // &lsq -> &lsqu + new Transition (4725, 4726), // <q -> <qu + new Transition (4743, 4750), // &lur -> &luru + new Transition (4767, 4952), // &m -> &mu + new Transition (4781, 4950), // &M -> &Mu + new Transition (4789, 4801), // &mapsto -> &mapstou + new Transition (4832, 4833), // &meas -> &measu + new Transition (4845, 4846), // &Medi -> &Mediu + new Transition (4890, 4891), // &min -> &minu + new Transition (4896, 4898), // &minusd -> &minusdu + new Transition (4901, 4902), // &Min -> &Minu + new Transition (4905, 4906), // &MinusPl -> &MinusPlu + new Transition (4918, 4919), // &mnpl -> &mnplu + new Transition (4965, 6032), // &n -> &nu + new Transition (4971, 6030), // &N -> &Nu + new Transition (4973, 4974), // &Nac -> &Nacu + new Transition (4978, 4979), // &nac -> &nacu + new Transition (5001, 5002), // &nat -> &natu + new Transition (5010, 5014), // &nb -> &nbu + new Transition (5020, 5052), // &nc -> &ncu + new Transition (5094, 5095), // &NegativeMedi -> &NegativeMediu + new Transition (5135, 5136), // &neq -> &nequ + new Transition (5380, 5390), // &NotC -> &NotCu + new Transition (5384, 5385), // &NotCongr -> &NotCongru + new Transition (5397, 5398), // &NotDo -> &NotDou + new Transition (5422, 5423), // &NotEq -> &NotEqu + new Transition (5448, 5449), // &NotGreaterEq -> &NotGreaterEqu + new Transition (5453, 5454), // &NotGreaterF -> &NotGreaterFu + new Transition (5458, 5459), // &NotGreaterFullEq -> &NotGreaterFullEqu + new Transition (5482, 5483), // &NotGreaterSlantEq -> &NotGreaterSlantEqu + new Transition (5493, 5494), // &NotH -> &NotHu + new Transition (5501, 5502), // &NotHumpDownH -> &NotHumpDownHu + new Transition (5507, 5508), // &NotHumpEq -> &NotHumpEqu + new Transition (5546, 5547), // &NotLeftTriangleEq -> &NotLeftTriangleEqu + new Transition (5555, 5556), // &NotLessEq -> &NotLessEqu + new Transition (5579, 5580), // &NotLessSlantEq -> &NotLessSlantEqu + new Transition (5640, 5641), // &NotPrecedesEq -> &NotPrecedesEqu + new Transition (5651, 5652), // &NotPrecedesSlantEq -> &NotPrecedesSlantEqu + new Transition (5689, 5690), // &NotRightTriangleEq -> &NotRightTriangleEqu + new Transition (5694, 5726), // &NotS -> &NotSu + new Transition (5695, 5696), // &NotSq -> &NotSqu + new Transition (5700, 5701), // &NotSquareS -> &NotSquareSu + new Transition (5708, 5709), // &NotSquareSubsetEq -> &NotSquareSubsetEqu + new Transition (5721, 5722), // &NotSquareSupersetEq -> &NotSquareSupersetEqu + new Transition (5733, 5734), // &NotSubsetEq -> &NotSubsetEqu + new Transition (5746, 5747), // &NotSucceedsEq -> &NotSucceedsEqu + new Transition (5757, 5758), // &NotSucceedsSlantEq -> &NotSucceedsSlantEqu + new Transition (5776, 5777), // &NotSupersetEq -> &NotSupersetEqu + new Transition (5788, 5789), // &NotTildeEq -> &NotTildeEqu + new Transition (5793, 5794), // &NotTildeF -> &NotTildeFu + new Transition (5798, 5799), // &NotTildeFullEq -> &NotTildeFullEqu + new Transition (5844, 5845), // &nprc -> &nprcu + new Transition (5895, 5951), // &ns -> &nsu + new Transition (5898, 5899), // &nscc -> &nsccu + new Transition (5943, 5944), // &nsqs -> &nsqsu + new Transition (6131, 6422), // &O -> &Ou + new Transition (6133, 6134), // &Oac -> &Oacu + new Transition (6138, 6426), // &o -> &ou + new Transition (6140, 6141), // &oac -> &oacu + new Transition (6290, 6291), // &omin -> &ominu + new Transition (6309, 6310), // &OpenC -> &OpenCu + new Transition (6315, 6316), // &OpenCurlyDo -> &OpenCurlyDou + new Transition (6320, 6321), // &OpenCurlyDoubleQ -> &OpenCurlyDoubleQu + new Transition (6326, 6327), // &OpenCurlyQ -> &OpenCurlyQu + new Transition (6336, 6337), // &opl -> &oplu + new Transition (6463, 6807), // &p -> &pu + new Transition (6555, 6566), // &pl -> &plu + new Transition (6580, 6583), // &plusd -> &plusdu + new Transition (6587, 6588), // &Pl -> &Plu + new Transition (6592, 6593), // &PlusMin -> &PlusMinu + new Transition (6622, 6636), // &po -> &pou + new Transition (6642, 6790), // &pr -> &pru + new Transition (6647, 6648), // &prc -> &prcu + new Transition (6664, 6665), // &precc -> &preccu + new Transition (6680, 6681), // &PrecedesEq -> &PrecedesEqu + new Transition (6691, 6692), // &PrecedesSlantEq -> &PrecedesSlantEqu + new Transition (6749, 6750), // &Prod -> &Produ + new Transition (6765, 6766), // &profs -> &profsu + new Transition (6817, 6847), // &q -> &qu + new Transition (6876, 7601), // &r -> &ru + new Transition (6883, 6893), // &rac -> &racu + new Transition (6886, 7590), // &R -> &Ru + new Transition (6888, 6889), // &Rac -> &Racu + new Transition (6921, 6922), // &raq -> &raqu + new Transition (7016, 7019), // &rbrksl -> &rbrkslu + new Transition (7027, 7046), // &rc -> &rcu + new Transition (7063, 7064), // &rdq -> &rdqu + new Transition (7110, 7111), // &ReverseEq -> &ReverseEqu + new Transition (7117, 7118), // &ReverseEquilibri -> &ReverseEquilibriu + new Transition (7124, 7125), // &ReverseUpEq -> &ReverseUpEqu + new Transition (7131, 7132), // &ReverseUpEquilibri -> &ReverseUpEquilibriu + new Transition (7157, 7160), // &rhar -> &rharu + new Transition (7237, 7238), // &RightDo -> &RightDou + new Transition (7285, 7291), // &rightharpoon -> &rightharpoonu + new Transition (7327, 7328), // &rightsq -> &rightsqu + new Transition (7378, 7379), // &RightTriangleEq -> &RightTriangleEqu + new Transition (7454, 7455), // &rmo -> &rmou + new Transition (7485, 7501), // &Ro -> &Rou + new Transition (7491, 7492), // &ropl -> &roplu + new Transition (7544, 7545), // &rsaq -> &rsaqu + new Transition (7559, 7562), // &rsq -> &rsqu + new Transition (7602, 7603), // &rul -> &rulu + new Transition (7610, 8127), // &S -> &Su + new Transition (7612, 7613), // &Sac -> &Sacu + new Transition (7617, 8130), // &s -> &su + new Transition (7619, 7620), // &sac -> &sacu + new Transition (7625, 7626), // &sbq -> &sbqu + new Transition (7645, 7646), // &scc -> &sccu + new Transition (7732, 7733), // &setmin -> &setminu + new Transition (7869, 7870), // &simpl -> &simplu + new Transition (7903, 7904), // &smallsetmin -> &smallsetminu + new Transition (7960, 7962), // &spades -> &spadesu + new Transition (7968, 8008), // &sq -> &squ + new Transition (7969, 7975), // &sqc -> &sqcu + new Transition (7980, 8010), // &Sq -> &Squ + new Transition (7984, 7985), // &sqs -> &sqsu + new Transition (8032, 8033), // &SquareS -> &SquareSu + new Transition (8040, 8041), // &SquareSubsetEq -> &SquareSubsetEqu + new Transition (8053, 8054), // &SquareSupersetEq -> &SquareSupersetEqu + new Transition (8145, 8146), // &subm -> &submu + new Transition (8156, 8157), // &subpl -> &subplu + new Transition (8169, 8193), // &subs -> &subsu + new Transition (8179, 8180), // &SubsetEq -> &SubsetEqu + new Transition (8208, 8209), // &succc -> &succcu + new Transition (8224, 8225), // &SucceedsEq -> &SucceedsEqu + new Transition (8235, 8236), // &SucceedsSlantEq -> &SucceedsSlantEqu + new Transition (8296, 8297), // &supds -> &supdsu + new Transition (8315, 8316), // &SupersetEq -> &SupersetEqu + new Transition (8321, 8325), // &suphs -> &suphsu + new Transition (8333, 8334), // &supm -> &supmu + new Transition (8344, 8345), // &suppl -> &supplu + new Transition (8352, 8370), // &sups -> &supsu + new Transition (8401, 8411), // &Ta -> &Tau + new Transition (8405, 8413), // &ta -> &tau + new Transition (8555, 8556), // &TildeEq -> &TildeEqu + new Transition (8560, 8561), // &TildeF -> &TildeFu + new Transition (8565, 8566), // &TildeFullEq -> &TildeFullEqu + new Transition (8672, 8673), // &trimin -> &triminu + new Transition (8686, 8687), // &tripl -> &triplu + new Transition (8701, 8702), // &trpezi -> &trpeziu + new Transition (8768, 9187), // &U -> &Uu + new Transition (8770, 8771), // &Uac -> &Uacu + new Transition (8775, 9182), // &u -> &uu + new Transition (8777, 8778), // &uac -> &uacu + new Transition (8950, 8951), // &UnionPl -> &UnionPlu + new Transition (8983, 9118), // &up -> &upu + new Transition (9035, 9036), // &UpEq -> &UpEqu + new Transition (9042, 9043), // &UpEquilibri -> &UpEquilibriu + new Transition (9064, 9065), // &upl -> &uplu + new Transition (9252, 9258), // &vars -> &varsu + new Transition (9426, 9427), // &vns -> &vnsu + new Transition (9454, 9458), // &vs -> &vsu + new Transition (9548, 9645), // &x -> &xu + new Transition (9549, 9557), // &xc -> &xcu + new Transition (9614, 9615), // &xopl -> &xoplu + new Transition (9641, 9642), // &xsqc -> &xsqcu + new Transition (9647, 9648), // &xupl -> &xuplu + new Transition (9665, 9740), // &Y -> &Yu + new Transition (9667, 9668), // &Yac -> &Yacu + new Transition (9672, 9736), // &y -> &yu + new Transition (9674, 9675), // &yac -> &yacu + new Transition (9749, 9750), // &Zac -> &Zacu + new Transition (9756, 9757) // &zac -> &zacu + }; + TransitionTable_v = new Transition[75] { + new Transition (0, 9201), // & -> &v + new Transition (17, 18), // &Abre -> &Abrev + new Transition (23, 24), // &abre -> &abrev + new Transition (69, 70), // &Agra -> &Agrav + new Transition (75, 76), // &agra -> &agrav + new Transition (120, 134), // &and -> &andv + new Transition (165, 167), // &angrt -> &angrtv + new Transition (341, 342), // &Bar -> &Barv + new Transition (344, 345), // &bar -> &barv + new Transition (402, 403), // &bempty -> &bemptyv + new Transition (443, 503), // &big -> &bigv + new Transition (584, 585), // &bnequi -> &bnequiv + new Transition (613, 693), // &box -> &boxv + new Transition (726, 727), // &Bre -> &Brev + new Transition (730, 735), // &br -> &brv + new Transition (731, 732), // &bre -> &brev + new Transition (930, 931), // &cempty -> &cemptyv + new Transition (1292, 1399), // &cu -> &cuv + new Transition (1346, 1381), // &cur -> &curv + new Transition (1354, 1367), // &curly -> &curlyv + new Transition (1455, 1461), // &dash -> &dashv + new Transition (1458, 1459), // &Dash -> &Dashv + new Transition (1532, 1533), // &dempty -> &demptyv + new Transition (1589, 1590), // &DiacriticalGra -> &DiacriticalGrav + new Transition (1599, 1643), // &di -> &div + new Transition (1917, 1918), // &DownBre -> &DownBrev + new Transition (2189, 2190), // &Egra -> &Egrav + new Transition (2194, 2195), // &egra -> &egrav + new Transition (2240, 2261), // &empty -> &emptyv + new Transition (2324, 2337), // &epsi -> &epsiv + new Transition (2339, 2402), // &eq -> &eqv + new Transition (2396, 2397), // &equi -> &equiv + new Transition (2626, 2628), // &fork -> &forkv + new Transition (2701, 3002), // &g -> &gv + new Transition (2726, 2727), // &Gbre -> &Gbrev + new Transition (2732, 2733), // &gbre -> &gbrev + new Transition (2862, 2863), // &gra -> &grav + new Transition (3291, 3292), // &Igra -> &Igrav + new Transition (3297, 3298), // &igra -> &igrav + new Transition (3398, 3444), // &In -> &Inv + new Transition (3512, 3524), // &isin -> &isinv + new Transition (3520, 3522), // &isins -> &isinsv + new Transition (3628, 3630), // &kappa -> &kappav + new Transition (3692, 4755), // &l -> &lv + new Transition (3715, 3716), // &laempty -> &laemptyv + new Transition (4965, 6043), // &n -> &nv + new Transition (5088, 5089), // &Negati -> &Negativ + new Transition (5137, 5138), // &nequi -> &nequiv + new Transition (5219, 5225), // &nGt -> &nGtv + new Transition (5240, 5246), // &ni -> &niv + new Transition (5332, 5341), // &nLt -> &nLtv + new Transition (5513, 5521), // ¬in -> ¬inv + new Transition (5621, 5623), // ¬ni -> ¬niv + new Transition (5657, 5658), // &NotRe -> &NotRev + new Transition (6131, 6435), // &O -> &Ov + new Transition (6138, 6430), // &o -> &ov + new Transition (6179, 6180), // &odi -> &odiv + new Transition (6216, 6217), // &Ogra -> &Ograv + new Transition (6221, 6222), // &ogra -> &ograv + new Transition (6342, 6374), // &or -> &orv + new Transition (6528, 6530), // &phi -> &phiv + new Transition (6543, 6553), // &pi -> &piv + new Transition (6563, 6564), // &plank -> &plankv + new Transition (6905, 6906), // &raempty -> &raemptyv + new Transition (7072, 7097), // &Re -> &Rev + new Transition (7167, 7169), // &rho -> &rhov + new Transition (7841, 7845), // &sigma -> &sigmav + new Transition (8485, 8491), // &theta -> &thetav + new Transition (8807, 8808), // &Ubre -> &Ubrev + new Transition (8811, 8812), // &ubre -> &ubrev + new Transition (8862, 8863), // &Ugra -> &Ugrav + new Transition (8868, 8869), // &ugra -> &ugrav + new Transition (9303, 9471), // &V -> &Vv + new Transition (9310, 9312), // &vBar -> &vBarv + new Transition (9548, 9655) // &x -> &xv + }; + TransitionTable_w = new Transition[137] { + new Transition (0, 9490), // & -> &w + new Transition (8, 289), // &a -> &aw + new Transition (341, 349), // &Bar -> &Barw + new Transition (344, 353), // &bar -> &barw + new Transition (426, 431), // &bet -> &betw + new Transition (443, 507), // &big -> &bigw + new Transition (490, 491), // &bigtriangledo -> &bigtriangledow + new Transition (516, 517), // &bkaro -> &bkarow + new Transition (548, 549), // &blacktriangledo -> &blacktriangledow + new Transition (598, 608), // &bo -> &bow + new Transition (796, 1407), // &c -> &cw + new Transition (991, 992), // &circlearro -> &circlearrow + new Transition (1071, 1072), // &Clock -> &Clockw + new Transition (1235, 1236), // &CounterClock -> &CounterClockw + new Transition (1292, 1403), // &cu -> &cuw + new Transition (1354, 1371), // &curly -> &curlyw + new Transition (1386, 1387), // &curvearro -> &curvearrow + new Transition (1432, 2086), // &d -> &dw + new Transition (1467, 1468), // &dbkaro -> &dbkarow + new Transition (1679, 1895), // &do -> &dow + new Transition (1685, 1881), // &Do -> &Dow + new Transition (1737, 1738), // &doublebar -> &doublebarw + new Transition (1765, 1768), // &DoubleDo -> &DoubleDow + new Transition (1773, 1774), // &DoubleDownArro -> &DoubleDownArrow + new Transition (1783, 1784), // &DoubleLeftArro -> &DoubleLeftArrow + new Transition (1794, 1795), // &DoubleLeftRightArro -> &DoubleLeftRightArrow + new Transition (1811, 1812), // &DoubleLongLeftArro -> &DoubleLongLeftArrow + new Transition (1822, 1823), // &DoubleLongLeftRightArro -> &DoubleLongLeftRightArrow + new Transition (1833, 1834), // &DoubleLongRightArro -> &DoubleLongRightArrow + new Transition (1844, 1845), // &DoubleRightArro -> &DoubleRightArrow + new Transition (1856, 1857), // &DoubleUpArro -> &DoubleUpArrow + new Transition (1860, 1861), // &DoubleUpDo -> &DoubleUpDow + new Transition (1866, 1867), // &DoubleUpDownArro -> &DoubleUpDownArrow + new Transition (1886, 1887), // &DownArro -> &DownArrow + new Transition (1892, 1893), // &Downarro -> &Downarrow + new Transition (1900, 1901), // &downarro -> &downarrow + new Transition (1912, 1913), // &DownArrowUpArro -> &DownArrowUpArrow + new Transition (1922, 1923), // &downdo -> &downdow + new Transition (1928, 1929), // &downdownarro -> &downdownarrow + new Transition (2020, 2021), // &DownTeeArro -> &DownTeeArrow + new Transition (2028, 2029), // &drbkaro -> &drbkarow + new Transition (2689, 2690), // &fro -> &frow + new Transition (3050, 3056), // &harr -> &harrw + new Transition (3113, 3120), // &hks -> &hksw + new Transition (3117, 3118), // &hksearo -> &hksearow + new Transition (3123, 3124), // &hkswaro -> &hkswarow + new Transition (3145, 3146), // &hookleftarro -> &hookleftarrow + new Transition (3156, 3157), // &hookrightarro -> &hookrightarrow + new Transition (3211, 3212), // &HumpDo -> &HumpDow + new Transition (3916, 3917), // &LeftArro -> &LeftArrow + new Transition (3922, 3923), // &Leftarro -> &Leftarrow + new Transition (3930, 3931), // &leftarro -> &leftarrow + new Transition (3945, 3946), // &LeftArrowRightArro -> &LeftArrowRightArrow + new Transition (3962, 3975), // &LeftDo -> &LeftDow + new Transition (4012, 4013), // &leftharpoondo -> &leftharpoondow + new Transition (4026, 4027), // &leftleftarro -> &leftleftarrow + new Transition (4038, 4039), // &LeftRightArro -> &LeftRightArrow + new Transition (4049, 4050), // &Leftrightarro -> &Leftrightarrow + new Transition (4060, 4061), // &leftrightarro -> &leftrightarrow + new Transition (4082, 4083), // &leftrightsquigarro -> &leftrightsquigarrow + new Transition (4099, 4100), // &LeftTeeArro -> &LeftTeeArrow + new Transition (4141, 4142), // &LeftUpDo -> &LeftUpDow + new Transition (4367, 4368), // &Lleftarro -> &Lleftarrow + new Transition (4422, 4579), // &lo -> &low + new Transition (4434, 4588), // &Lo -> &Low + new Transition (4444, 4445), // &LongLeftArro -> &LongLeftArrow + new Transition (4454, 4455), // &Longleftarro -> &Longleftarrow + new Transition (4466, 4467), // &longleftarro -> &longleftarrow + new Transition (4477, 4478), // &LongLeftRightArro -> &LongLeftRightArrow + new Transition (4488, 4489), // &Longleftrightarro -> &Longleftrightarrow + new Transition (4499, 4500), // &longleftrightarro -> &longleftrightarrow + new Transition (4517, 4518), // &LongRightArro -> &LongRightArrow + new Transition (4528, 4529), // &Longrightarro -> &Longrightarrow + new Transition (4539, 4540), // &longrightarro -> &longrightarrow + new Transition (4547, 4548), // &looparro -> &looparrow + new Transition (4598, 4599), // &LowerLeftArro -> &LowerLeftArrow + new Transition (4609, 4610), // &LowerRightArro -> &LowerRightArrow + new Transition (4792, 4793), // &mapstodo -> &mapstodow + new Transition (4965, 6111), // &n -> &nw + new Transition (5077, 5078), // &nearro -> &nearrow + new Transition (5084, 5176), // &Ne -> &New + new Transition (5279, 5280), // &nLeftarro -> &nLeftarrow + new Transition (5287, 5288), // &nleftarro -> &nleftarrow + new Transition (5298, 5299), // &nLeftrightarro -> &nLeftrightarrow + new Transition (5309, 5310), // &nleftrightarro -> &nleftrightarrow + new Transition (5498, 5499), // &NotHumpDo -> &NotHumpDow + new Transition (5862, 5866), // &nrarr -> &nrarrw + new Transition (5876, 5877), // &nRightarro -> &nRightarrow + new Transition (5886, 5887), // &nrightarro -> &nrightarrow + new Transition (6123, 6124), // &nwarro -> &nwarrow + new Transition (6603, 6604), // &plust -> &plustw + new Transition (6932, 6966), // &rarr -> &rarrw + new Transition (7190, 7191), // &RightArro -> &RightArrow + new Transition (7196, 7197), // &Rightarro -> &Rightarrow + new Transition (7206, 7207), // &rightarro -> &rightarrow + new Transition (7220, 7221), // &RightArrowLeftArro -> &RightArrowLeftArrow + new Transition (7237, 7250), // &RightDo -> &RightDow + new Transition (7287, 7288), // &rightharpoondo -> &rightharpoondow + new Transition (7301, 7302), // &rightleftarro -> &rightleftarrow + new Transition (7322, 7323), // &rightrightarro -> &rightrightarrow + new Transition (7334, 7335), // &rightsquigarro -> &rightsquigarrow + new Transition (7344, 7345), // &RightTeeArro -> &RightTeeArrow + new Transition (7386, 7387), // &RightUpDo -> &RightUpDow + new Transition (7539, 7540), // &Rrightarro -> &Rrightarrow + new Transition (7617, 8375), // &s -> &sw + new Transition (7715, 7716), // &searro -> &searrow + new Transition (7724, 7725), // &ses -> &sesw + new Transition (7747, 7748), // &sfro -> &sfrow + new Transition (7777, 7778), // &ShortDo -> &ShortDow + new Transition (7783, 7784), // &ShortDownArro -> &ShortDownArrow + new Transition (7793, 7794), // &ShortLeftArro -> &ShortLeftArrow + new Transition (7820, 7821), // &ShortRightArro -> &ShortRightArrow + new Transition (7828, 7829), // &ShortUpArro -> &ShortUpArrow + new Transition (8387, 8388), // &swarro -> &swarrow + new Transition (8390, 8391), // &swn -> &swnw + new Transition (8404, 8737), // &t -> &tw + new Transition (8641, 8642), // &triangledo -> &triangledow + new Transition (8754, 8755), // &twoheadleftarro -> &twoheadleftarrow + new Transition (8765, 8766), // &twoheadrightarro -> &twoheadrightarrow + new Transition (8775, 9194), // &u -> &uw + new Transition (8974, 8975), // &UpArro -> &UpArrow + new Transition (8980, 8981), // &Uparro -> &Uparrow + new Transition (8987, 8988), // &uparro -> &uparrow + new Transition (8995, 8996), // &UpArrowDo -> &UpArrowDow + new Transition (9001, 9002), // &UpArrowDownArro -> &UpArrowDownArrow + new Transition (9005, 9006), // &UpDo -> &UpDow + new Transition (9011, 9012), // &UpDownArro -> &UpDownArrow + new Transition (9015, 9016), // &Updo -> &Updow + new Transition (9021, 9022), // &Updownarro -> &Updownarrow + new Transition (9025, 9026), // &updo -> &updow + new Transition (9031, 9032), // &updownarro -> &updownarrow + new Transition (9078, 9079), // &UpperLeftArro -> &UpperLeftArrow + new Transition (9089, 9090), // &UpperRightArro -> &UpperRightArrow + new Transition (9115, 9116), // &UpTeeArro -> &UpTeeArrow + new Transition (9123, 9124), // &upuparro -> &upuparrow + new Transition (9548, 9659), // &x -> &xw + new Transition (9754, 9848) // &z -> &zw + }; + TransitionTable_x = new Transition[24] { + new Transition (0, 9548), // & -> &x + new Transition (231, 232), // &appro -> &approx + new Transition (598, 613), // &bo -> &box + new Transition (615, 616), // &boxbo -> &boxbox + new Transition (1154, 1160), // &comple -> &complex + new Transition (1658, 1659), // &divon -> &divonx + new Transition (2108, 2466), // &E -> &Ex + new Transition (2115, 2458), // &e -> &ex + new Transition (2838, 2839), // &gnappro -> &gnapprox + new Transition (2970, 2971), // >rappro -> >rapprox + new Transition (3273, 3277), // &ie -> &iex + new Transition (4220, 4221), // &lessappro -> &lessapprox + new Transition (4407, 4408), // &lnappro -> &lnapprox + new Transition (4998, 4999), // &nappro -> &napprox + new Transition (5064, 5182), // &ne -> &nex + new Transition (5414, 5433), // &NotE -> &NotEx + new Transition (6661, 6662), // &precappro -> &precapprox + new Transition (6710, 6711), // &precnappro -> &precnapprox + new Transition (6876, 7608), // &r -> &rx + new Transition (7703, 7738), // &se -> &sex + new Transition (8205, 8206), // &succappro -> &succapprox + new Transition (8254, 8255), // &succnappro -> &succnapprox + new Transition (8500, 8501), // &thickappro -> &thickapprox + new Transition (8738, 8739) // &twi -> &twix + }; + TransitionTable_y = new Transition[122] { + new Transition (0, 9672), // & -> &y + new Transition (27, 48), // &ac -> &acy + new Transition (33, 46), // &Ac -> &Acy + new Transition (82, 83), // &alefs -> &alefsy + new Transition (218, 219), // &Appl -> &Apply + new Transition (251, 262), // &as -> &asy + new Transition (369, 377), // &bc -> &bcy + new Transition (374, 375), // &Bc -> &Bcy + new Transition (401, 402), // &bempt -> &bempty + new Transition (790, 855), // &Ca -> &Cay + new Transition (796, 1419), // &c -> &cy + new Transition (857, 858), // &Cayle -> &Cayley + new Transition (929, 930), // &cempt -> &cempty + new Transition (957, 958), // &CHc -> &CHcy + new Transition (961, 962), // &chc -> &chcy + new Transition (1097, 1098), // &CloseCurl -> &CloseCurly + new Transition (1203, 1221), // &cop -> © + new Transition (1353, 1354), // &curl -> &curly + new Transition (1422, 1423), // &cylct -> &cylcty + new Transition (1474, 1486), // &Dc -> &Dcy + new Transition (1480, 1488), // &dc -> &dcy + new Transition (1531, 1532), // &dempt -> &dempty + new Transition (1662, 1663), // &DJc -> &DJcy + new Transition (1666, 1667), // &djc -> &djcy + new Transition (2045, 2052), // &dsc -> &dscy + new Transition (2049, 2050), // &DSc -> &DScy + new Transition (2094, 2095), // &DZc -> &DZcy + new Transition (2098, 2099), // &dzc -> &dzcy + new Transition (2127, 2153), // &Ec -> &Ecy + new Transition (2133, 2155), // &ec -> &ecy + new Transition (2239, 2240), // &empt -> &empty + new Transition (2247, 2248), // &Empt -> &Empty + new Transition (2265, 2266), // &EmptyVer -> &EmptyVery + new Transition (2518, 2519), // &Fc -> &Fcy + new Transition (2521, 2522), // &fc -> &fcy + new Transition (2573, 2574), // &FilledVer -> &FilledVery + new Transition (2736, 2751), // &Gc -> &Gcy + new Transition (2746, 2753), // &gc -> &gcy + new Transition (2817, 2818), // &GJc -> &GJcy + new Transition (2821, 2822), // &gjc -> &gjcy + new Transition (3020, 3225), // &h -> &hy + new Transition (3038, 3039), // &HARDc -> &HARDcy + new Transition (3043, 3044), // &hardc -> &hardcy + new Transition (3250, 3263), // &ic -> &icy + new Transition (3252, 3261), // &Ic -> &Icy + new Transition (3270, 3271), // &IEc -> &IEcy + new Transition (3274, 3275), // &iec -> &iecy + new Transition (3348, 3349), // &Imaginar -> &Imaginary + new Transition (3464, 3465), // &IOc -> &IOcy + new Transition (3468, 3469), // &ioc -> &iocy + new Transition (3541, 3542), // &Iukc -> &Iukcy + new Transition (3546, 3547), // &iukc -> &iukcy + new Transition (3556, 3567), // &Jc -> &Jcy + new Transition (3562, 3569), // &jc -> &jcy + new Transition (3600, 3601), // &Jserc -> &Jsercy + new Transition (3605, 3606), // &jserc -> &jsercy + new Transition (3610, 3611), // &Jukc -> &Jukcy + new Transition (3615, 3616), // &jukc -> &jukcy + new Transition (3632, 3644), // &Kc -> &Kcy + new Transition (3638, 3646), // &kc -> &kcy + new Transition (3661, 3662), // &KHc -> &KHcy + new Transition (3665, 3666), // &khc -> &khcy + new Transition (3669, 3670), // &KJc -> &KJcy + new Transition (3673, 3674), // &kjc -> &kjcy + new Transition (3714, 3715), // &laempt -> &laempty + new Transition (3837, 3865), // &Lc -> &Lcy + new Transition (3843, 3867), // &lc -> &lcy + new Transition (4339, 4340), // &LJc -> &LJcy + new Transition (4343, 4344), // &ljc -> &ljcy + new Transition (4809, 4818), // &mc -> &mcy + new Transition (4815, 4816), // &Mc -> &Mcy + new Transition (5020, 5057), // &nc -> &ncy + new Transition (5024, 5055), // &Nc -> &Ncy + new Transition (5123, 5124), // &NegativeVer -> &NegativeVery + new Transition (5249, 5250), // &NJc -> &NJcy + new Transition (5253, 5254), // &njc -> &njcy + new Transition (6148, 6161), // &oc -> &ocy + new Transition (6152, 6159), // &Oc -> &Ocy + new Transition (6312, 6313), // &OpenCurl -> &OpenCurly + new Transition (6491, 6492), // &Pc -> &Pcy + new Transition (6494, 6495), // &pc -> &pcy + new Transition (6667, 6668), // &preccurl -> &preccurly + new Transition (6904, 6905), // &raempt -> &raempty + new Transition (7021, 7049), // &Rc -> &Rcy + new Transition (7027, 7051), // &rc -> &rcy + new Transition (7596, 7597), // &RuleDela -> &RuleDelay + new Transition (7629, 7691), // &Sc -> &Scy + new Transition (7631, 7693), // &sc -> &scy + new Transition (7751, 7831), // &sh -> ­ + new Transition (7759, 7760), // &SHCHc -> &SHCHcy + new Transition (7762, 7770), // &shc -> &shcy + new Transition (7764, 7765), // &shchc -> &shchcy + new Transition (7767, 7768), // &SHc -> &SHcy + new Transition (7933, 7934), // &SOFTc -> &SOFTcy + new Transition (7939, 7940), // &softc -> &softcy + new Transition (8211, 8212), // &succcurl -> &succcurly + new Transition (8419, 8441), // &Tc -> &Tcy + new Transition (8425, 8443), // &tc -> &tcy + new Transition (8487, 8488), // &thetas -> &thetasy + new Transition (8710, 8717), // &tsc -> &tscy + new Transition (8714, 8715), // &TSc -> &TScy + new Transition (8720, 8721), // &TSHc -> &TSHcy + new Transition (8724, 8725), // &tshc -> &tshcy + new Transition (8799, 8800), // &Ubrc -> &Ubrcy + new Transition (8804, 8805), // &ubrc -> &ubrcy + new Transition (8815, 8825), // &Uc -> &Ucy + new Transition (8820, 8827), // &uc -> &ucy + new Transition (9314, 9315), // &Vc -> &Vcy + new Transition (9317, 9318), // &vc -> &vcy + new Transition (9360, 9403), // &Ver -> &Very + new Transition (9674, 9683), // &yac -> &yacy + new Transition (9680, 9681), // &YAc -> &YAcy + new Transition (9685, 9695), // &Yc -> &Ycy + new Transition (9690, 9697), // &yc -> &ycy + new Transition (9709, 9710), // &YIc -> &YIcy + new Transition (9713, 9714), // &yic -> &yicy + new Transition (9733, 9734), // &YUc -> &YUcy + new Transition (9737, 9738), // &yuc -> &yucy + new Transition (9761, 9773), // &Zc -> &Zcy + new Transition (9767, 9775), // &zc -> &zcy + new Transition (9818, 9819), // &ZHc -> &ZHcy + new Transition (9822, 9823) // &zhc -> &zhcy + }; + TransitionTable_z = new Transition[10] { + new Transition (0, 9754), // & -> &z + new Transition (136, 178), // &ang -> &angz + new Transition (524, 525), // &blacklo -> &blackloz + new Transition (1432, 2097), // &d -> &dz + new Transition (3172, 3173), // &Hori -> &Horiz + new Transition (4422, 4612), // &lo -> &loz + new Transition (7617, 8395), // &s -> &sz + new Transition (8699, 8700), // &trpe -> &trpez + new Transition (9201, 9477), // &v -> &vz + new Transition (9479, 9480) // &vzig -> &vzigz + }; + + NamedEntities = new Dictionary { + [6] = "\u00C1", // Á + [7] = "\u00C1", // Á + [13] = "\u00E1", // á + [14] = "\u00E1", // á + [20] = "\u0102", // Ă + [26] = "\u0103", // ă + [28] = "\u223E", // ∾ + [30] = "\u223F", // ∿ + [32] = "\u223E\u0333", // ∾̳ + [36] = "\u00C2", //  + [37] = "\u00C2", //  + [40] = "\u00E2", // â + [41] = "\u00E2", // â + [44] = "\u00B4", // ´ + [45] = "\u00B4", // ´ + [47] = "\u0410", // А + [49] = "\u0430", // а + [53] = "\u00C6", // Æ + [54] = "\u00C6", // Æ + [58] = "\u00E6", // æ + [59] = "\u00E6", // æ + [61] = "\u2061", // ⁡ + [64] = "\uD835\uDD04", // 𝔄 + [66] = "\uD835\uDD1E", // 𝔞 + [71] = "\u00C0", // À + [72] = "\u00C0", // À + [77] = "\u00E0", // à + [78] = "\u00E0", // à + [85] = "\u2135", // ℵ + [88] = "\u2135", // ℵ + [93] = "\u0391", // Α + [97] = "\u03B1", // α + [102] = "\u0100", // Ā + [107] = "\u0101", // ā + [110] = "\u2A3F", // ⨿ + [112] = "\u0026", // & + [113] = "\u0026", // & + [114] = "\u0026", // & + [115] = "\u0026", // & + [118] = "\u2A53", // ⩓ + [121] = "\u2227", // ∧ + [125] = "\u2A55", // ⩕ + [127] = "\u2A5C", // ⩜ + [133] = "\u2A58", // ⩘ + [135] = "\u2A5A", // ⩚ + [137] = "\u2220", // ∠ + [139] = "\u29A4", // ⦤ + [142] = "\u2220", // ∠ + [146] = "\u2221", // ∡ + [149] = "\u29A8", // ⦨ + [151] = "\u29A9", // ⦩ + [153] = "\u29AA", // ⦪ + [155] = "\u29AB", // ⦫ + [157] = "\u29AC", // ⦬ + [159] = "\u29AD", // ⦭ + [161] = "\u29AE", // ⦮ + [163] = "\u29AF", // ⦯ + [166] = "\u221F", // ∟ + [169] = "\u22BE", // ⊾ + [171] = "\u299D", // ⦝ + [175] = "\u2222", // ∢ + [177] = "\u00C5", // Å + [182] = "\u237C", // ⍼ + [187] = "\u0104", // Ą + [192] = "\u0105", // ą + [195] = "\uD835\uDD38", // 𝔸 + [198] = "\uD835\uDD52", // 𝕒 + [200] = "\u2248", // ≈ + [205] = "\u2A6F", // ⩯ + [207] = "\u2A70", // ⩰ + [209] = "\u224A", // ≊ + [212] = "\u224B", // ≋ + [215] = "\u0027", // ' + [228] = "\u2061", // ⁡ + [233] = "\u2248", // ≈ + [236] = "\u224A", // ≊ + [240] = "\u00C5", // Å + [241] = "\u00C5", // Å + [245] = "\u00E5", // å + [246] = "\u00E5", // å + [250] = "\uD835\uDC9C", // 𝒜 + [254] = "\uD835\uDCB6", // 𝒶 + [259] = "\u2254", // ≔ + [261] = "\u002A", // * + [265] = "\u2248", // ≈ + [268] = "\u224D", // ≍ + [273] = "\u00C3", // à + [274] = "\u00C3", // à + [279] = "\u00E3", // ã + [280] = "\u00E3", // ã + [283] = "\u00C4", // Ä + [284] = "\u00C4", // Ä + [287] = "\u00E4", // ä + [288] = "\u00E4", // ä + [296] = "\u2233", // ∳ + [300] = "\u2A11", // ⨑ + [309] = "\u224C", // ≌ + [317] = "\u03F6", // ϶ + [323] = "\u2035", // ‵ + [327] = "\u223D", // ∽ + [330] = "\u22CD", // ⋍ + [340] = "\u2216", // ∖ + [343] = "\u2AE7", // ⫧ + [348] = "\u22BD", // ⊽ + [352] = "\u2306", // ⌆ + [356] = "\u2305", // ⌅ + [359] = "\u2305", // ⌅ + [363] = "\u23B5", // ⎵ + [368] = "\u23B6", // ⎶ + [373] = "\u224C", // ≌ + [376] = "\u0411", // Б + [378] = "\u0431", // б + [383] = "\u201E", // „ + [389] = "\u2235", // ∵ + [396] = "\u2235", // ∵ + [398] = "\u2235", // ∵ + [404] = "\u29B0", // ⦰ + [408] = "\u03F6", // ϶ + [413] = "\u212C", // ℬ + [422] = "\u212C", // ℬ + [425] = "\u0392", // Β + [428] = "\u03B2", // β + [430] = "\u2136", // ℶ + [435] = "\u226C", // ≬ + [438] = "\uD835\uDD05", // 𝔅 + [441] = "\uD835\uDD1F", // 𝔟 + [447] = "\u22C2", // ⋂ + [451] = "\u25EF", // ◯ + [454] = "\u22C3", // ⋃ + [459] = "\u2A00", // ⨀ + [464] = "\u2A01", // ⨁ + [470] = "\u2A02", // ⨂ + [476] = "\u2A06", // ⨆ + [480] = "\u2605", // ★ + [493] = "\u25BD", // ▽ + [496] = "\u25B3", // △ + [502] = "\u2A04", // ⨄ + [506] = "\u22C1", // ⋁ + [512] = "\u22C0", // ⋀ + [518] = "\u290D", // ⤍ + [530] = "\u29EB", // ⧫ + [537] = "\u25AA", // ▪ + [546] = "\u25B4", // ▴ + [551] = "\u25BE", // ▾ + [556] = "\u25C2", // ◂ + [562] = "\u25B8", // ▸ + [565] = "\u2423", // ␣ + [569] = "\u2592", // ▒ + [571] = "\u2591", // ░ + [574] = "\u2593", // ▓ + [578] = "\u2588", // █ + [581] = "\u003D\u20E5", // =⃥ + [586] = "\u2261\u20E5", // ≡⃥ + [590] = "\u2AED", // ⫭ + [593] = "\u2310", // ⌐ + [597] = "\uD835\uDD39", // 𝔹 + [601] = "\uD835\uDD53", // 𝕓 + [603] = "\u22A5", // ⊥ + [607] = "\u22A5", // ⊥ + [612] = "\u22C8", // ⋈ + [617] = "\u29C9", // ⧉ + [620] = "\u2557", // ╗ + [622] = "\u2556", // ╖ + [625] = "\u2555", // ╕ + [627] = "\u2510", // ┐ + [629] = "\u2554", // ╔ + [631] = "\u2553", // ╓ + [633] = "\u2552", // ╒ + [635] = "\u250C", // ┌ + [637] = "\u2550", // ═ + [639] = "\u2500", // ─ + [641] = "\u2566", // ╦ + [643] = "\u2564", // ╤ + [645] = "\u2565", // ╥ + [647] = "\u252C", // ┬ + [649] = "\u2569", // ╩ + [651] = "\u2567", // ╧ + [653] = "\u2568", // ╨ + [655] = "\u2534", // ┴ + [661] = "\u229F", // ⊟ + [666] = "\u229E", // ⊞ + [672] = "\u22A0", // ⊠ + [675] = "\u255D", // ╝ + [677] = "\u255C", // ╜ + [680] = "\u255B", // ╛ + [682] = "\u2518", // ┘ + [684] = "\u255A", // ╚ + [686] = "\u2559", // ╙ + [688] = "\u2558", // ╘ + [690] = "\u2514", // └ + [692] = "\u2551", // ║ + [694] = "\u2502", // │ + [696] = "\u256C", // ╬ + [698] = "\u256B", // ╫ + [700] = "\u256A", // ╪ + [702] = "\u253C", // ┼ + [704] = "\u2563", // ╣ + [706] = "\u2562", // ╢ + [708] = "\u2561", // ╡ + [710] = "\u2524", // ┤ + [712] = "\u2560", // ╠ + [714] = "\u255F", // ╟ + [716] = "\u255E", // ╞ + [718] = "\u251C", // ├ + [724] = "\u2035", // ‵ + [729] = "\u02D8", // ˘ + [734] = "\u02D8", // ˘ + [738] = "\u00A6", // ¦ + [739] = "\u00A6", // ¦ + [743] = "\u212C", // ℬ + [747] = "\uD835\uDCB7", // 𝒷 + [751] = "\u204F", // ⁏ + [754] = "\u223D", // ∽ + [756] = "\u22CD", // ⋍ + [759] = "\u005C", // \ + [761] = "\u29C5", // ⧅ + [766] = "\u27C8", // ⟈ + [770] = "\u2022", // • + [773] = "\u2022", // • + [776] = "\u224E", // ≎ + [778] = "\u2AAE", // ⪮ + [780] = "\u224F", // ≏ + [786] = "\u224E", // ≎ + [788] = "\u224F", // ≏ + [795] = "\u0106", // Ć + [802] = "\u0107", // ć + [804] = "\u22D2", // ⋒ + [806] = "\u2229", // ∩ + [810] = "\u2A44", // ⩄ + [816] = "\u2A49", // ⩉ + [820] = "\u2A4B", // ⩋ + [823] = "\u2A47", // ⩇ + [827] = "\u2A40", // ⩀ + [845] = "\u2145", // ⅅ + [847] = "\u2229\uFE00", // ∩︀ + [851] = "\u2041", // ⁁ + [854] = "\u02C7", // ˇ + [860] = "\u212D", // ℭ + [865] = "\u2A4D", // ⩍ + [871] = "\u010C", // Č + [875] = "\u010D", // č + [879] = "\u00C7", // Ç + [880] = "\u00C7", // Ç + [884] = "\u00E7", // ç + [885] = "\u00E7", // ç + [889] = "\u0108", // Ĉ + [893] = "\u0109", // ĉ + [899] = "\u2230", // ∰ + [903] = "\u2A4C", // ⩌ + [906] = "\u2A50", // ⩐ + [910] = "\u010A", // Ċ + [914] = "\u010B", // ċ + [918] = "\u00B8", // ¸ + [919] = "\u00B8", // ¸ + [926] = "\u00B8", // ¸ + [932] = "\u29B2", // ⦲ + [934] = "\u00A2", // ¢ + [935] = "\u00A2", // ¢ + [943] = "\u00B7", // · + [949] = "\u00B7", // · + [952] = "\u212D", // ℭ + [955] = "\uD835\uDD20", // 𝔠 + [959] = "\u0427", // Ч + [963] = "\u0447", // ч + [967] = "\u2713", // ✓ + [972] = "\u2713", // ✓ + [975] = "\u03A7", // Χ + [977] = "\u03C7", // χ + [980] = "\u25CB", // ○ + [982] = "\u02C6", // ˆ + [985] = "\u2257", // ≗ + [997] = "\u21BA", // ↺ + [1003] = "\u21BB", // ↻ + [1008] = "\u229B", // ⊛ + [1013] = "\u229A", // ⊚ + [1018] = "\u229D", // ⊝ + [1027] = "\u2299", // ⊙ + [1029] = "\u00AE", // ® + [1031] = "\u24C8", // Ⓢ + [1037] = "\u2296", // ⊖ + [1042] = "\u2295", // ⊕ + [1048] = "\u2297", // ⊗ + [1050] = "\u29C3", // ⧃ + [1052] = "\u2257", // ≗ + [1058] = "\u2A10", // ⨐ + [1062] = "\u2AEF", // ⫯ + [1067] = "\u29C2", // ⧂ + [1091] = "\u2232", // ∲ + [1110] = "\u201D", // ” + [1116] = "\u2019", // ’ + [1121] = "\u2663", // ♣ + [1125] = "\u2663", // ♣ + [1130] = "\u2237", // ∷ + [1135] = "\u003A", // : + [1137] = "\u2A74", // ⩴ + [1139] = "\u2254", // ≔ + [1141] = "\u2254", // ≔ + [1145] = "\u002C", // , + [1147] = "\u0040", // @ + [1149] = "\u2201", // ∁ + [1152] = "\u2218", // ∘ + [1159] = "\u2201", // ∁ + [1163] = "\u2102", // ℂ + [1166] = "\u2245", // ≅ + [1170] = "\u2A6D", // ⩭ + [1178] = "\u2261", // ≡ + [1182] = "\u222F", // ∯ + [1186] = "\u222E", // ∮ + [1199] = "\u222E", // ∮ + [1202] = "\u2102", // ℂ + [1205] = "\uD835\uDD54", // 𝕔 + [1209] = "\u2210", // ∐ + [1216] = "\u2210", // ∐ + [1219] = "\u00A9", // © + [1220] = "\u00A9", // © + [1221] = "\u00A9", // © + [1222] = "\u00A9", // © + [1225] = "\u2117", // ℗ + [1255] = "\u2233", // ∳ + [1260] = "\u21B5", // ↵ + [1265] = "\u2A2F", // ⨯ + [1269] = "\u2717", // ✗ + [1273] = "\uD835\uDC9E", // 𝒞 + [1277] = "\uD835\uDCB8", // 𝒸 + [1280] = "\u2ACF", // ⫏ + [1282] = "\u2AD1", // ⫑ + [1284] = "\u2AD0", // ⫐ + [1286] = "\u2AD2", // ⫒ + [1291] = "\u22EF", // ⋯ + [1298] = "\u2938", // ⤸ + [1300] = "\u2935", // ⤵ + [1304] = "\u22DE", // ⋞ + [1307] = "\u22DF", // ⋟ + [1312] = "\u21B6", // ↶ + [1314] = "\u293D", // ⤽ + [1317] = "\u22D3", // ⋓ + [1319] = "\u222A", // ∪ + [1325] = "\u2A48", // ⩈ + [1329] = "\u224D", // ≍ + [1333] = "\u2A46", // ⩆ + [1336] = "\u2A4A", // ⩊ + [1340] = "\u228D", // ⊍ + [1343] = "\u2A45", // ⩅ + [1345] = "\u222A\uFE00", // ∪︀ + [1350] = "\u21B7", // ↷ + [1352] = "\u293C", // ⤼ + [1361] = "\u22DE", // ⋞ + [1366] = "\u22DF", // ⋟ + [1370] = "\u22CE", // ⋎ + [1376] = "\u22CF", // ⋏ + [1379] = "\u00A4", // ¤ + [1380] = "\u00A4", // ¤ + [1392] = "\u21B6", // ↶ + [1398] = "\u21B7", // ↷ + [1402] = "\u22CE", // ⋎ + [1406] = "\u22CF", // ⋏ + [1414] = "\u2232", // ∲ + [1418] = "\u2231", // ∱ + [1424] = "\u232D", // ⌭ + [1431] = "\u2021", // ‡ + [1438] = "\u2020", // † + [1443] = "\u2138", // ℸ + [1446] = "\u21A1", // ↡ + [1450] = "\u21D3", // ⇓ + [1453] = "\u2193", // ↓ + [1456] = "\u2010", // ‐ + [1460] = "\u2AE4", // ⫤ + [1462] = "\u22A3", // ⊣ + [1469] = "\u290F", // ⤏ + [1473] = "\u02DD", // ˝ + [1479] = "\u010E", // Ď + [1485] = "\u010F", // ď + [1487] = "\u0414", // Д + [1489] = "\u0434", // д + [1491] = "\u2145", // ⅅ + [1493] = "\u2146", // ⅆ + [1499] = "\u2021", // ‡ + [1502] = "\u21CA", // ⇊ + [1509] = "\u2911", // ⤑ + [1515] = "\u2A77", // ⩷ + [1517] = "\u00B0", // ° + [1518] = "\u00B0", // ° + [1521] = "\u2207", // ∇ + [1524] = "\u0394", // Δ + [1528] = "\u03B4", // δ + [1534] = "\u29B1", // ⦱ + [1540] = "\u297F", // ⥿ + [1543] = "\uD835\uDD07", // 𝔇 + [1545] = "\uD835\uDD21", // 𝔡 + [1549] = "\u2965", // ⥥ + [1554] = "\u21C3", // ⇃ + [1556] = "\u21C2", // ⇂ + [1572] = "\u00B4", // ´ + [1576] = "\u02D9", // ˙ + [1586] = "\u02DD", // ˝ + [1592] = "\u0060", // ` + [1598] = "\u02DC", // ˜ + [1602] = "\u22C4", // ⋄ + [1607] = "\u22C4", // ⋄ + [1611] = "\u22C4", // ⋄ + [1616] = "\u2666", // ♦ + [1618] = "\u2666", // ♦ + [1620] = "\u00A8", // ¨ + [1632] = "\u2146", // ⅆ + [1638] = "\u03DD", // ϝ + [1642] = "\u22F2", // ⋲ + [1644] = "\u00F7", // ÷ + [1647] = "\u00F7", // ÷ + [1648] = "\u00F7", // ÷ + [1656] = "\u22C7", // ⋇ + [1660] = "\u22C7", // ⋇ + [1664] = "\u0402", // Ђ + [1668] = "\u0452", // ђ + [1674] = "\u231E", // ⌞ + [1678] = "\u230D", // ⌍ + [1684] = "\u0024", // $ + [1688] = "\uD835\uDD3B", // 𝔻 + [1691] = "\uD835\uDD55", // 𝕕 + [1693] = "\u00A8", // ¨ + [1695] = "\u02D9", // ˙ + [1699] = "\u20DC", // ⃜ + [1702] = "\u2250", // ≐ + [1706] = "\u2251", // ≑ + [1712] = "\u2250", // ≐ + [1718] = "\u2238", // ∸ + [1723] = "\u2214", // ∔ + [1730] = "\u22A1", // ⊡ + [1743] = "\u2306", // ⌆ + [1763] = "\u222F", // ∯ + [1767] = "\u00A8", // ¨ + [1775] = "\u21D3", // ⇓ + [1785] = "\u21D0", // ⇐ + [1796] = "\u21D4", // ⇔ + [1800] = "\u2AE4", // ⫤ + [1813] = "\u27F8", // ⟸ + [1824] = "\u27FA", // ⟺ + [1835] = "\u27F9", // ⟹ + [1846] = "\u21D2", // ⇒ + [1850] = "\u22A8", // ⊨ + [1858] = "\u21D1", // ⇑ + [1868] = "\u21D5", // ⇕ + [1880] = "\u2225", // ∥ + [1888] = "\u2193", // ↓ + [1894] = "\u21D3", // ⇓ + [1902] = "\u2193", // ↓ + [1906] = "\u2913", // ⤓ + [1914] = "\u21F5", // ⇵ + [1920] = "\u0311", // ̑ + [1931] = "\u21CA", // ⇊ + [1943] = "\u21C3", // ⇃ + [1949] = "\u21C2", // ⇂ + [1965] = "\u2950", // ⥐ + [1975] = "\u295E", // ⥞ + [1982] = "\u21BD", // ↽ + [1986] = "\u2956", // ⥖ + [2001] = "\u295F", // ⥟ + [2008] = "\u21C1", // ⇁ + [2012] = "\u2957", // ⥗ + [2016] = "\u22A4", // ⊤ + [2022] = "\u21A7", // ↧ + [2030] = "\u2910", // ⤐ + [2035] = "\u231F", // ⌟ + [2039] = "\u230C", // ⌌ + [2043] = "\uD835\uDC9F", // 𝒟 + [2047] = "\uD835\uDCB9", // 𝒹 + [2051] = "\u0405", // Ѕ + [2053] = "\u0455", // ѕ + [2056] = "\u29F6", // ⧶ + [2061] = "\u0110", // Đ + [2066] = "\u0111", // đ + [2071] = "\u22F1", // ⋱ + [2074] = "\u25BF", // ▿ + [2076] = "\u25BE", // ▾ + [2081] = "\u21F5", // ⇵ + [2085] = "\u296F", // ⥯ + [2092] = "\u29A6", // ⦦ + [2096] = "\u040F", // Џ + [2100] = "\u045F", // џ + [2107] = "\u27FF", // ⟿ + [2113] = "\u00C9", // É + [2114] = "\u00C9", // É + [2120] = "\u00E9", // é + [2121] = "\u00E9", // é + [2126] = "\u2A6E", // ⩮ + [2132] = "\u011A", // Ě + [2138] = "\u011B", // ě + [2141] = "\u2256", // ≖ + [2144] = "\u00CA", // Ê + [2145] = "\u00CA", // Ê + [2146] = "\u00EA", // ê + [2147] = "\u00EA", // ê + [2152] = "\u2255", // ≕ + [2154] = "\u042D", // Э + [2156] = "\u044D", // э + [2161] = "\u2A77", // ⩷ + [2165] = "\u0116", // Ė + [2168] = "\u2251", // ≑ + [2172] = "\u0117", // ė + [2174] = "\u2147", // ⅇ + [2179] = "\u2252", // ≒ + [2182] = "\uD835\uDD08", // 𝔈 + [2184] = "\uD835\uDD22", // 𝔢 + [2186] = "\u2A9A", // ⪚ + [2191] = "\u00C8", // È + [2192] = "\u00C8", // È + [2196] = "\u00E8", // è + [2197] = "\u00E8", // è + [2199] = "\u2A96", // ⪖ + [2203] = "\u2A98", // ⪘ + [2205] = "\u2A99", // ⪙ + [2212] = "\u2208", // ∈ + [2219] = "\u23E7", // ⏧ + [2221] = "\u2113", // ℓ + [2223] = "\u2A95", // ⪕ + [2227] = "\u2A97", // ⪗ + [2232] = "\u0112", // Ē + [2237] = "\u0113", // ē + [2241] = "\u2205", // ∅ + [2245] = "\u2205", // ∅ + [2260] = "\u25FB", // ◻ + [2262] = "\u2205", // ∅ + [2278] = "\u25AB", // ▫ + [2281] = "\u2003", //   + [2284] = "\u2004", //   + [2286] = "\u2005", //   + [2289] = "\u014A", // Ŋ + [2292] = "\u014B", // ŋ + [2295] = "\u2002", //   + [2300] = "\u0118", // Ę + [2305] = "\u0119", // ę + [2308] = "\uD835\uDD3C", // 𝔼 + [2311] = "\uD835\uDD56", // 𝕖 + [2315] = "\u22D5", // ⋕ + [2318] = "\u29E3", // ⧣ + [2322] = "\u2A71", // ⩱ + [2325] = "\u03B5", // ε + [2332] = "\u0395", // Ε + [2336] = "\u03B5", // ε + [2338] = "\u03F5", // ϵ + [2344] = "\u2256", // ≖ + [2349] = "\u2255", // ≕ + [2353] = "\u2242", // ≂ + [2361] = "\u2A96", // ⪖ + [2366] = "\u2A95", // ⪕ + [2371] = "\u2A75", // ⩵ + [2376] = "\u003D", // = + [2382] = "\u2242", // ≂ + [2386] = "\u225F", // ≟ + [2395] = "\u21CC", // ⇌ + [2398] = "\u2261", // ≡ + [2401] = "\u2A78", // ⩸ + [2408] = "\u29E5", // ⧥ + [2413] = "\u2971", // ⥱ + [2417] = "\u2253", // ≓ + [2421] = "\u2130", // ℰ + [2425] = "\u212F", // ℯ + [2429] = "\u2250", // ≐ + [2432] = "\u2A73", // ⩳ + [2435] = "\u2242", // ≂ + [2438] = "\u0397", // Η + [2441] = "\u03B7", // η + [2443] = "\u00D0", // Ð + [2444] = "\u00D0", // Ð + [2445] = "\u00F0", // ð + [2446] = "\u00F0", // ð + [2449] = "\u00CB", // Ë + [2450] = "\u00CB", // Ë + [2453] = "\u00EB", // ë + [2454] = "\u00EB", // ë + [2457] = "\u20AC", // € + [2461] = "\u0021", // ! + [2465] = "\u2203", // ∃ + [2471] = "\u2203", // ∃ + [2481] = "\u2130", // ℰ + [2492] = "\u2147", // ⅇ + [2502] = "\u2147", // ⅇ + [2516] = "\u2252", // ≒ + [2520] = "\u0424", // Ф + [2523] = "\u0444", // ф + [2529] = "\u2640", // ♀ + [2535] = "\uFB03", // ffi + [2539] = "\uFB00", // ff + [2543] = "\uFB04", // ffl + [2546] = "\uD835\uDD09", // 𝔉 + [2548] = "\uD835\uDD23", // 𝔣 + [2553] = "\uFB01", // fi + [2570] = "\u25FC", // ◼ + [2586] = "\u25AA", // ▪ + [2591] = "\u0066\u006A", // fj + [2595] = "\u266D", // ♭ + [2599] = "\uFB02", // fl + [2603] = "\u25B1", // ▱ + [2607] = "\u0192", // ƒ + [2611] = "\uD835\uDD3D", // 𝔽 + [2615] = "\uD835\uDD57", // 𝕗 + [2620] = "\u2200", // ∀ + [2625] = "\u2200", // ∀ + [2627] = "\u22D4", // ⋔ + [2629] = "\u2AD9", // ⫙ + [2638] = "\u2131", // ℱ + [2646] = "\u2A0D", // ⨍ + [2651] = "\u00BD", // ½ + [2652] = "\u00BD", // ½ + [2654] = "\u2153", // ⅓ + [2655] = "\u00BC", // ¼ + [2656] = "\u00BC", // ¼ + [2658] = "\u2155", // ⅕ + [2660] = "\u2159", // ⅙ + [2662] = "\u215B", // ⅛ + [2665] = "\u2154", // ⅔ + [2667] = "\u2156", // ⅖ + [2669] = "\u00BE", // ¾ + [2670] = "\u00BE", // ¾ + [2672] = "\u2157", // ⅗ + [2674] = "\u215C", // ⅜ + [2677] = "\u2158", // ⅘ + [2680] = "\u215A", // ⅚ + [2682] = "\u215D", // ⅝ + [2685] = "\u215E", // ⅞ + [2688] = "\u2044", // ⁄ + [2692] = "\u2322", // ⌢ + [2696] = "\u2131", // ℱ + [2700] = "\uD835\uDCBB", // 𝒻 + [2707] = "\u01F5", // ǵ + [2713] = "\u0393", // Γ + [2717] = "\u03B3", // γ + [2719] = "\u03DC", // Ϝ + [2721] = "\u03DD", // ϝ + [2723] = "\u2A86", // ⪆ + [2729] = "\u011E", // Ğ + [2735] = "\u011F", // ğ + [2741] = "\u0122", // Ģ + [2745] = "\u011C", // Ĝ + [2750] = "\u011D", // ĝ + [2752] = "\u0413", // Г + [2754] = "\u0433", // г + [2758] = "\u0120", // Ġ + [2762] = "\u0121", // ġ + [2764] = "\u2267", // ≧ + [2766] = "\u2265", // ≥ + [2768] = "\u2A8C", // ⪌ + [2770] = "\u22DB", // ⋛ + [2772] = "\u2265", // ≥ + [2774] = "\u2267", // ≧ + [2780] = "\u2A7E", // ⩾ + [2782] = "\u2A7E", // ⩾ + [2785] = "\u2AA9", // ⪩ + [2789] = "\u2A80", // ⪀ + [2791] = "\u2A82", // ⪂ + [2793] = "\u2A84", // ⪄ + [2795] = "\u22DB\uFE00", // ⋛︀ + [2798] = "\u2A94", // ⪔ + [2801] = "\uD835\uDD0A", // 𝔊 + [2804] = "\uD835\uDD24", // 𝔤 + [2806] = "\u22D9", // ⋙ + [2808] = "\u226B", // ≫ + [2810] = "\u22D9", // ⋙ + [2815] = "\u2137", // ℷ + [2819] = "\u0403", // Ѓ + [2823] = "\u0453", // ѓ + [2825] = "\u2277", // ≷ + [2827] = "\u2AA5", // ⪥ + [2829] = "\u2A92", // ⪒ + [2831] = "\u2AA4", // ⪤ + [2835] = "\u2A8A", // ⪊ + [2840] = "\u2A8A", // ⪊ + [2842] = "\u2269", // ≩ + [2844] = "\u2A88", // ⪈ + [2846] = "\u2A88", // ⪈ + [2848] = "\u2269", // ≩ + [2852] = "\u22E7", // ⋧ + [2856] = "\uD835\uDD3E", // 𝔾 + [2860] = "\uD835\uDD58", // 𝕘 + [2865] = "\u0060", // ` + [2877] = "\u2265", // ≥ + [2882] = "\u22DB", // ⋛ + [2892] = "\u2267", // ≧ + [2900] = "\u2AA2", // ⪢ + [2905] = "\u2277", // ≷ + [2916] = "\u2A7E", // ⩾ + [2922] = "\u2273", // ≳ + [2926] = "\uD835\uDCA2", // 𝒢 + [2930] = "\u210A", // ℊ + [2933] = "\u2273", // ≳ + [2935] = "\u2A8E", // ⪎ + [2937] = "\u2A90", // ⪐ + [2938] = "\u003E", // > + [2939] = "\u003E", // > + [2941] = "\u226B", // ≫ + [2942] = "\u003E", // > + [2943] = "\u003E", // > + [2946] = "\u2AA7", // ⪧ + [2949] = "\u2A7A", // ⩺ + [2953] = "\u22D7", // ⋗ + [2958] = "\u2995", // ⦕ + [2964] = "\u2A7C", // ⩼ + [2972] = "\u2A86", // ⪆ + [2975] = "\u2978", // ⥸ + [2979] = "\u22D7", // ⋗ + [2986] = "\u22DB", // ⋛ + [2992] = "\u2A8C", // ⪌ + [2997] = "\u2277", // ≷ + [3001] = "\u2273", // ≳ + [3010] = "\u2269\uFE00", // ≩︀ + [3013] = "\u2269\uFE00", // ≩︀ + [3019] = "\u02C7", // ˇ + [3026] = "\u200A", //   + [3029] = "\u00BD", // ½ + [3034] = "\u210B", // ℋ + [3040] = "\u042A", // Ъ + [3045] = "\u044A", // ъ + [3049] = "\u21D4", // ⇔ + [3051] = "\u2194", // ↔ + [3055] = "\u2948", // ⥈ + [3057] = "\u21AD", // ↭ + [3059] = "\u005E", // ^ + [3063] = "\u210F", // ℏ + [3068] = "\u0124", // Ĥ + [3073] = "\u0125", // ĥ + [3079] = "\u2665", // ♥ + [3083] = "\u2665", // ♥ + [3088] = "\u2026", // … + [3093] = "\u22B9", // ⊹ + [3096] = "\u210C", // ℌ + [3099] = "\uD835\uDD25", // 𝔥 + [3111] = "\u210B", // ℋ + [3119] = "\u2925", // ⤥ + [3125] = "\u2926", // ⤦ + [3130] = "\u21FF", // ⇿ + [3135] = "\u223B", // ∻ + [3147] = "\u21A9", // ↩ + [3158] = "\u21AA", // ↪ + [3162] = "\u210D", // ℍ + [3165] = "\uD835\uDD59", // 𝕙 + [3170] = "\u2015", // ― + [3183] = "\u2500", // ─ + [3187] = "\u210B", // ℋ + [3191] = "\uD835\uDCBD", // 𝒽 + [3196] = "\u210F", // ℏ + [3201] = "\u0126", // Ħ + [3206] = "\u0127", // ħ + [3218] = "\u224E", // ≎ + [3224] = "\u224F", // ≏ + [3230] = "\u2043", // ⁃ + [3235] = "\u2010", // ‐ + [3241] = "\u00CD", // Í + [3242] = "\u00CD", // Í + [3248] = "\u00ED", // í + [3249] = "\u00ED", // í + [3251] = "\u2063", // ⁣ + [3255] = "\u00CE", // Î + [3256] = "\u00CE", // Î + [3259] = "\u00EE", // î + [3260] = "\u00EE", // î + [3262] = "\u0418", // И + [3264] = "\u0438", // и + [3268] = "\u0130", // İ + [3272] = "\u0415", // Е + [3276] = "\u0435", // е + [3279] = "\u00A1", // ¡ + [3280] = "\u00A1", // ¡ + [3283] = "\u21D4", // ⇔ + [3286] = "\u2111", // ℑ + [3288] = "\uD835\uDD26", // 𝔦 + [3293] = "\u00CC", // Ì + [3294] = "\u00CC", // Ì + [3299] = "\u00EC", // ì + [3300] = "\u00EC", // ì + [3302] = "\u2148", // ⅈ + [3307] = "\u2A0C", // ⨌ + [3310] = "\u222D", // ∭ + [3315] = "\u29DC", // ⧜ + [3319] = "\u2129", // ℩ + [3324] = "\u0132", // IJ + [3329] = "\u0133", // ij + [3331] = "\u2111", // ℑ + [3335] = "\u012A", // Ī + [3340] = "\u012B", // ī + [3343] = "\u2111", // ℑ + [3351] = "\u2148", // ⅈ + [3356] = "\u2110", // ℐ + [3361] = "\u2111", // ℑ + [3364] = "\u0131", // ı + [3367] = "\u22B7", // ⊷ + [3371] = "\u01B5", // Ƶ + [3377] = "\u21D2", // ⇒ + [3379] = "\u2208", // ∈ + [3384] = "\u2105", // ℅ + [3388] = "\u221E", // ∞ + [3392] = "\u29DD", // ⧝ + [3397] = "\u0131", // ı + [3400] = "\u222C", // ∬ + [3402] = "\u222B", // ∫ + [3406] = "\u22BA", // ⊺ + [3412] = "\u2124", // ℤ + [3418] = "\u222B", // ∫ + [3423] = "\u22BA", // ⊺ + [3432] = "\u22C2", // ⋂ + [3438] = "\u2A17", // ⨗ + [3443] = "\u2A3C", // ⨼ + [3456] = "\u2063", // ⁣ + [3462] = "\u2062", // ⁢ + [3466] = "\u0401", // Ё + [3470] = "\u0451", // ё + [3475] = "\u012E", // Į + [3479] = "\u012F", // į + [3482] = "\uD835\uDD40", // 𝕀 + [3485] = "\uD835\uDD5A", // 𝕚 + [3488] = "\u0399", // Ι + [3491] = "\u03B9", // ι + [3496] = "\u2A3C", // ⨼ + [3501] = "\u00BF", // ¿ + [3502] = "\u00BF", // ¿ + [3506] = "\u2110", // ℐ + [3510] = "\uD835\uDCBE", // 𝒾 + [3513] = "\u2208", // ∈ + [3517] = "\u22F5", // ⋵ + [3519] = "\u22F9", // ⋹ + [3521] = "\u22F4", // ⋴ + [3523] = "\u22F3", // ⋳ + [3525] = "\u2208", // ∈ + [3527] = "\u2062", // ⁢ + [3533] = "\u0128", // Ĩ + [3538] = "\u0129", // ĩ + [3543] = "\u0406", // І + [3548] = "\u0456", // і + [3550] = "\u00CF", // Ï + [3551] = "\u00CF", // Ï + [3553] = "\u00EF", // ï + [3554] = "\u00EF", // ï + [3560] = "\u0134", // Ĵ + [3566] = "\u0135", // ĵ + [3568] = "\u0419", // Й + [3570] = "\u0439", // й + [3573] = "\uD835\uDD0D", // 𝔍 + [3576] = "\uD835\uDD27", // 𝔧 + [3581] = "\u0237", // ȷ + [3585] = "\uD835\uDD41", // 𝕁 + [3589] = "\uD835\uDD5B", // 𝕛 + [3593] = "\uD835\uDCA5", // 𝒥 + [3597] = "\uD835\uDCBF", // 𝒿 + [3602] = "\u0408", // Ј + [3607] = "\u0458", // ј + [3612] = "\u0404", // Є + [3617] = "\u0454", // є + [3623] = "\u039A", // Κ + [3629] = "\u03BA", // κ + [3631] = "\u03F0", // ϰ + [3637] = "\u0136", // Ķ + [3643] = "\u0137", // ķ + [3645] = "\u041A", // К + [3647] = "\u043A", // к + [3650] = "\uD835\uDD0E", // 𝔎 + [3653] = "\uD835\uDD28", // 𝔨 + [3659] = "\u0138", // ĸ + [3663] = "\u0425", // Х + [3667] = "\u0445", // х + [3671] = "\u040C", // Ќ + [3675] = "\u045C", // ќ + [3679] = "\uD835\uDD42", // 𝕂 + [3683] = "\uD835\uDD5C", // 𝕜 + [3687] = "\uD835\uDCA6", // 𝒦 + [3691] = "\uD835\uDCC0", // 𝓀 + [3697] = "\u21DA", // ⇚ + [3704] = "\u0139", // Ĺ + [3710] = "\u013A", // ĺ + [3717] = "\u29B4", // ⦴ + [3722] = "\u2112", // ℒ + [3727] = "\u039B", // Λ + [3732] = "\u03BB", // λ + [3735] = "\u27EA", // ⟪ + [3738] = "\u27E8", // ⟨ + [3740] = "\u2991", // ⦑ + [3743] = "\u27E8", // ⟨ + [3745] = "\u2A85", // ⪅ + [3754] = "\u2112", // ℒ + [3757] = "\u00AB", // « + [3758] = "\u00AB", // « + [3761] = "\u219E", // ↞ + [3764] = "\u21D0", // ⇐ + [3767] = "\u2190", // ← + [3769] = "\u21E4", // ⇤ + [3772] = "\u291F", // ⤟ + [3775] = "\u291D", // ⤝ + [3778] = "\u21A9", // ↩ + [3781] = "\u21AB", // ↫ + [3784] = "\u2939", // ⤹ + [3788] = "\u2973", // ⥳ + [3791] = "\u21A2", // ↢ + [3793] = "\u2AAB", // ⪫ + [3798] = "\u291B", // ⤛ + [3802] = "\u2919", // ⤙ + [3804] = "\u2AAD", // ⪭ + [3806] = "\u2AAD\uFE00", // ⪭︀ + [3811] = "\u290E", // ⤎ + [3816] = "\u290C", // ⤌ + [3820] = "\u2772", // ❲ + [3825] = "\u007B", // { + [3827] = "\u005B", // [ + [3830] = "\u298B", // ⦋ + [3834] = "\u298F", // ⦏ + [3836] = "\u298D", // ⦍ + [3842] = "\u013D", // Ľ + [3848] = "\u013E", // ľ + [3853] = "\u013B", // Ļ + [3858] = "\u013C", // ļ + [3861] = "\u2308", // ⌈ + [3864] = "\u007B", // { + [3866] = "\u041B", // Л + [3868] = "\u043B", // л + [3872] = "\u2936", // ⤶ + [3876] = "\u201C", // “ + [3878] = "\u201E", // „ + [3884] = "\u2967", // ⥧ + [3890] = "\u294B", // ⥋ + [3893] = "\u21B2", // ↲ + [3895] = "\u2266", // ≦ + [3897] = "\u2264", // ≤ + [3913] = "\u27E8", // ⟨ + [3918] = "\u2190", // ← + [3924] = "\u21D0", // ⇐ + [3932] = "\u2190", // ← + [3936] = "\u21E4", // ⇤ + [3947] = "\u21C6", // ⇆ + [3952] = "\u21A2", // ↢ + [3960] = "\u2308", // ⌈ + [3974] = "\u27E6", // ⟦ + [3986] = "\u2961", // ⥡ + [3993] = "\u21C3", // ⇃ + [3997] = "\u2959", // ⥙ + [4003] = "\u230A", // ⌊ + [4015] = "\u21BD", // ↽ + [4018] = "\u21BC", // ↼ + [4029] = "\u21C7", // ⇇ + [4040] = "\u2194", // ↔ + [4051] = "\u21D4", // ⇔ + [4062] = "\u2194", // ↔ + [4064] = "\u21C6", // ⇆ + [4073] = "\u21CB", // ⇋ + [4084] = "\u21AD", // ↭ + [4091] = "\u294E", // ⥎ + [4095] = "\u22A3", // ⊣ + [4101] = "\u21A4", // ↤ + [4108] = "\u295A", // ⥚ + [4119] = "\u22CB", // ⋋ + [4127] = "\u22B2", // ⊲ + [4131] = "\u29CF", // ⧏ + [4137] = "\u22B4", // ⊴ + [4150] = "\u2951", // ⥑ + [4160] = "\u2960", // ⥠ + [4167] = "\u21BF", // ↿ + [4171] = "\u2958", // ⥘ + [4178] = "\u21BC", // ↼ + [4182] = "\u2952", // ⥒ + [4184] = "\u2A8B", // ⪋ + [4186] = "\u22DA", // ⋚ + [4188] = "\u2264", // ≤ + [4190] = "\u2266", // ≦ + [4196] = "\u2A7D", // ⩽ + [4198] = "\u2A7D", // ⩽ + [4201] = "\u2AA8", // ⪨ + [4205] = "\u2A7F", // ⩿ + [4207] = "\u2A81", // ⪁ + [4209] = "\u2A83", // ⪃ + [4211] = "\u22DA\uFE00", // ⋚︀ + [4214] = "\u2A93", // ⪓ + [4222] = "\u2A85", // ⪅ + [4226] = "\u22D6", // ⋖ + [4232] = "\u22DA", // ⋚ + [4237] = "\u2A8B", // ⪋ + [4252] = "\u22DA", // ⋚ + [4262] = "\u2266", // ≦ + [4270] = "\u2276", // ≶ + [4274] = "\u2276", // ≶ + [4279] = "\u2AA1", // ⪡ + [4283] = "\u2272", // ≲ + [4294] = "\u2A7D", // ⩽ + [4300] = "\u2272", // ≲ + [4306] = "\u297C", // ⥼ + [4311] = "\u230A", // ⌊ + [4314] = "\uD835\uDD0F", // 𝔏 + [4316] = "\uD835\uDD29", // 𝔩 + [4318] = "\u2276", // ≶ + [4320] = "\u2A91", // ⪑ + [4324] = "\u2962", // ⥢ + [4329] = "\u21BD", // ↽ + [4331] = "\u21BC", // ↼ + [4333] = "\u296A", // ⥪ + [4337] = "\u2584", // ▄ + [4341] = "\u0409", // Љ + [4345] = "\u0459", // љ + [4347] = "\u22D8", // ⋘ + [4349] = "\u226A", // ≪ + [4353] = "\u21C7", // ⇇ + [4360] = "\u231E", // ⌞ + [4369] = "\u21DA", // ⇚ + [4374] = "\u296B", // ⥫ + [4378] = "\u25FA", // ◺ + [4384] = "\u013F", // Ŀ + [4390] = "\u0140", // ŀ + [4395] = "\u23B0", // ⎰ + [4400] = "\u23B0", // ⎰ + [4404] = "\u2A89", // ⪉ + [4409] = "\u2A89", // ⪉ + [4411] = "\u2268", // ≨ + [4413] = "\u2A87", // ⪇ + [4415] = "\u2A87", // ⪇ + [4417] = "\u2268", // ≨ + [4421] = "\u22E6", // ⋦ + [4426] = "\u27EC", // ⟬ + [4429] = "\u21FD", // ⇽ + [4433] = "\u27E6", // ⟦ + [4446] = "\u27F5", // ⟵ + [4456] = "\u27F8", // ⟸ + [4468] = "\u27F5", // ⟵ + [4479] = "\u27F7", // ⟷ + [4490] = "\u27FA", // ⟺ + [4501] = "\u27F7", // ⟷ + [4508] = "\u27FC", // ⟼ + [4519] = "\u27F6", // ⟶ + [4530] = "\u27F9", // ⟹ + [4541] = "\u27F6", // ⟶ + [4553] = "\u21AB", // ↫ + [4559] = "\u21AC", // ↬ + [4563] = "\u2985", // ⦅ + [4566] = "\uD835\uDD43", // 𝕃 + [4568] = "\uD835\uDD5D", // 𝕝 + [4572] = "\u2A2D", // ⨭ + [4578] = "\u2A34", // ⨴ + [4583] = "\u2217", // ∗ + [4587] = "\u005F", // _ + [4600] = "\u2199", // ↙ + [4611] = "\u2198", // ↘ + [4613] = "\u25CA", // ◊ + [4618] = "\u25CA", // ◊ + [4620] = "\u29EB", // ⧫ + [4624] = "\u0028", // ( + [4627] = "\u2993", // ⦓ + [4632] = "\u21C6", // ⇆ + [4639] = "\u231F", // ⌟ + [4643] = "\u21CB", // ⇋ + [4645] = "\u296D", // ⥭ + [4647] = "\u200E", // ‎ + [4651] = "\u22BF", // ⊿ + [4657] = "\u2039", // ‹ + [4661] = "\u2112", // ℒ + [4664] = "\uD835\uDCC1", // 𝓁 + [4666] = "\u21B0", // ↰ + [4668] = "\u21B0", // ↰ + [4671] = "\u2272", // ≲ + [4673] = "\u2A8D", // ⪍ + [4675] = "\u2A8F", // ⪏ + [4678] = "\u005B", // [ + [4681] = "\u2018", // ‘ + [4683] = "\u201A", // ‚ + [4688] = "\u0141", // Ł + [4693] = "\u0142", // ł + [4694] = "\u003C", // < + [4695] = "\u003C", // < + [4697] = "\u226A", // ≪ + [4698] = "\u003C", // < + [4699] = "\u003C", // < + [4702] = "\u2AA6", // ⪦ + [4705] = "\u2A79", // ⩹ + [4709] = "\u22D6", // ⋖ + [4714] = "\u22CB", // ⋋ + [4719] = "\u22C9", // ⋉ + [4724] = "\u2976", // ⥶ + [4730] = "\u2A7B", // ⩻ + [4733] = "\u25C3", // ◃ + [4735] = "\u22B4", // ⊴ + [4737] = "\u25C2", // ◂ + [4741] = "\u2996", // ⦖ + [4749] = "\u294A", // ⥊ + [4754] = "\u2966", // ⥦ + [4763] = "\u2268\uFE00", // ≨︀ + [4766] = "\u2268\uFE00", // ≨︀ + [4770] = "\u00AF", // ¯ + [4771] = "\u00AF", // ¯ + [4774] = "\u2642", // ♂ + [4776] = "\u2720", // ✠ + [4780] = "\u2720", // ✠ + [4784] = "\u2905", // ⤅ + [4786] = "\u21A6", // ↦ + [4790] = "\u21A6", // ↦ + [4795] = "\u21A7", // ↧ + [4800] = "\u21A4", // ↤ + [4803] = "\u21A5", // ↥ + [4808] = "\u25AE", // ▮ + [4814] = "\u2A29", // ⨩ + [4817] = "\u041C", // М + [4819] = "\u043C", // м + [4824] = "\u2014", // — + [4829] = "\u223A", // ∺ + [4842] = "\u2221", // ∡ + [4853] = "\u205F", //   + [4861] = "\u2133", // ℳ + [4864] = "\uD835\uDD10", // 𝔐 + [4867] = "\uD835\uDD2A", // 𝔪 + [4870] = "\u2127", // ℧ + [4874] = "\u00B5", // µ + [4875] = "\u00B5", // µ + [4877] = "\u2223", // ∣ + [4881] = "\u002A", // * + [4885] = "\u2AF0", // ⫰ + [4888] = "\u00B7", // · + [4889] = "\u00B7", // · + [4893] = "\u2212", // − + [4895] = "\u229F", // ⊟ + [4897] = "\u2238", // ∸ + [4899] = "\u2A2A", // ⨪ + [4908] = "\u2213", // ∓ + [4912] = "\u2ADB", // ⫛ + [4915] = "\u2026", // … + [4921] = "\u2213", // ∓ + [4927] = "\u22A7", // ⊧ + [4931] = "\uD835\uDD44", // 𝕄 + [4934] = "\uD835\uDD5E", // 𝕞 + [4936] = "\u2213", // ∓ + [4940] = "\u2133", // ℳ + [4944] = "\uD835\uDCC2", // 𝓂 + [4949] = "\u223E", // ∾ + [4951] = "\u039C", // Μ + [4953] = "\u03BC", // μ + [4960] = "\u22B8", // ⊸ + [4964] = "\u22B8", // ⊸ + [4970] = "\u2207", // ∇ + [4977] = "\u0143", // Ń + [4982] = "\u0144", // ń + [4985] = "\u2220\u20D2", // ∠⃒ + [4987] = "\u2249", // ≉ + [4989] = "\u2A70\u0338", // ⩰̸ + [4992] = "\u224B\u0338", // ≋̸ + [4995] = "\u0149", // ʼn + [5000] = "\u2249", // ≉ + [5004] = "\u266E", // ♮ + [5007] = "\u266E", // ♮ + [5009] = "\u2115", // ℕ + [5012] = "\u00A0", //   + [5013] = "\u00A0", //   + [5017] = "\u224E\u0338", // ≎̸ + [5019] = "\u224F\u0338", // ≏̸ + [5023] = "\u2A43", // ⩃ + [5029] = "\u0147", // Ň + [5033] = "\u0148", // ň + [5038] = "\u0145", // Ņ + [5043] = "\u0146", // ņ + [5047] = "\u2247", // ≇ + [5051] = "\u2A6D\u0338", // ⩭̸ + [5054] = "\u2A42", // ⩂ + [5056] = "\u041D", // Н + [5058] = "\u043D", // н + [5063] = "\u2013", // – + [5065] = "\u2260", // ≠ + [5070] = "\u2924", // ⤤ + [5074] = "\u21D7", // ⇗ + [5076] = "\u2197", // ↗ + [5079] = "\u2197", // ↗ + [5083] = "\u2250\u0338", // ≐̸ + [5102] = "\u200B", // ​ + [5113] = "\u200B", // ​ + [5120] = "\u200B", // ​ + [5134] = "\u200B", // ​ + [5139] = "\u2262", // ≢ + [5144] = "\u2928", // ⤨ + [5147] = "\u2242\u0338", // ≂̸ + [5166] = "\u226B", // ≫ + [5175] = "\u226A", // ≪ + [5181] = "\u000A", // + [5186] = "\u2204", // ∄ + [5188] = "\u2204", // ∄ + [5191] = "\uD835\uDD11", // 𝔑 + [5194] = "\uD835\uDD2B", // 𝔫 + [5197] = "\u2267\u0338", // ≧̸ + [5199] = "\u2271", // ≱ + [5201] = "\u2271", // ≱ + [5203] = "\u2267\u0338", // ≧̸ + [5209] = "\u2A7E\u0338", // ⩾̸ + [5211] = "\u2A7E\u0338", // ⩾̸ + [5214] = "\u22D9\u0338", // ⋙̸ + [5218] = "\u2275", // ≵ + [5220] = "\u226B\u20D2", // ≫⃒ + [5222] = "\u226F", // ≯ + [5224] = "\u226F", // ≯ + [5226] = "\u226B\u0338", // ≫̸ + [5231] = "\u21CE", // ⇎ + [5235] = "\u21AE", // ↮ + [5239] = "\u2AF2", // ⫲ + [5241] = "\u220B", // ∋ + [5243] = "\u22FC", // ⋼ + [5245] = "\u22FA", // ⋺ + [5247] = "\u220B", // ∋ + [5251] = "\u040A", // Њ + [5255] = "\u045A", // њ + [5260] = "\u21CD", // ⇍ + [5264] = "\u219A", // ↚ + [5267] = "\u2025", // ‥ + [5269] = "\u2266\u0338", // ≦̸ + [5271] = "\u2270", // ≰ + [5281] = "\u21CD", // ⇍ + [5289] = "\u219A", // ↚ + [5300] = "\u21CE", // ⇎ + [5311] = "\u21AE", // ↮ + [5313] = "\u2270", // ≰ + [5315] = "\u2266\u0338", // ≦̸ + [5321] = "\u2A7D\u0338", // ⩽̸ + [5323] = "\u2A7D\u0338", // ⩽̸ + [5325] = "\u226E", // ≮ + [5327] = "\u22D8\u0338", // ⋘̸ + [5331] = "\u2274", // ≴ + [5333] = "\u226A\u20D2", // ≪⃒ + [5335] = "\u226E", // ≮ + [5338] = "\u22EA", // ⋪ + [5340] = "\u22EC", // ⋬ + [5342] = "\u226A\u0338", // ≪̸ + [5346] = "\u2224", // ∤ + [5353] = "\u2060", // ⁠ + [5368] = "\u00A0", //   + [5371] = "\u2115", // ℕ + [5375] = "\uD835\uDD5F", // 𝕟 + [5377] = "\u2AEC", // ⫬ + [5378] = "\u00AC", // ¬ + [5379] = "\u00AC", // ¬ + [5389] = "\u2262", // ≢ + [5395] = "\u226D", // ≭ + [5413] = "\u2226", // ∦ + [5421] = "\u2209", // ∉ + [5426] = "\u2260", // ≠ + [5432] = "\u2242\u0338", // ≂̸ + [5438] = "\u2204", // ∄ + [5446] = "\u226F", // ≯ + [5452] = "\u2271", // ≱ + [5462] = "\u2267\u0338", // ≧̸ + [5470] = "\u226B\u0338", // ≫̸ + [5475] = "\u2279", // ≹ + [5486] = "\u2A7E\u0338", // ⩾̸ + [5492] = "\u2275", // ≵ + [5505] = "\u224E\u0338", // ≎̸ + [5511] = "\u224F\u0338", // ≏̸ + [5514] = "\u2209", // ∉ + [5518] = "\u22F5\u0338", // ⋵̸ + [5520] = "\u22F9\u0338", // ⋹̸ + [5523] = "\u2209", // ∉ + [5525] = "\u22F7", // ⋷ + [5527] = "\u22F6", // ⋶ + [5540] = "\u22EA", // ⋪ + [5544] = "\u29CF\u0338", // ⧏̸ + [5550] = "\u22EC", // ⋬ + [5553] = "\u226E", // ≮ + [5559] = "\u2270", // ≰ + [5567] = "\u2278", // ≸ + [5572] = "\u226A\u0338", // ≪̸ + [5583] = "\u2A7D\u0338", // ⩽̸ + [5589] = "\u2274", // ≴ + [5610] = "\u2AA2\u0338", // ⪢̸ + [5619] = "\u2AA1\u0338", // ⪡̸ + [5622] = "\u220C", // ∌ + [5625] = "\u220C", // ∌ + [5627] = "\u22FE", // ⋾ + [5629] = "\u22FD", // ⋽ + [5638] = "\u2280", // ⊀ + [5644] = "\u2AAF\u0338", // ⪯̸ + [5655] = "\u22E0", // ⋠ + [5670] = "\u220C", // ∌ + [5683] = "\u22EB", // ⋫ + [5687] = "\u29D0\u0338", // ⧐̸ + [5693] = "\u22ED", // ⋭ + [5706] = "\u228F\u0338", // ⊏̸ + [5712] = "\u22E2", // ⋢ + [5719] = "\u2290\u0338", // ⊐̸ + [5725] = "\u22E3", // ⋣ + [5731] = "\u2282\u20D2", // ⊂⃒ + [5737] = "\u2288", // ⊈ + [5744] = "\u2281", // ⊁ + [5750] = "\u2AB0\u0338", // ⪰̸ + [5761] = "\u22E1", // ⋡ + [5767] = "\u227F\u0338", // ≿̸ + [5774] = "\u2283\u20D2", // ⊃⃒ + [5780] = "\u2289", // ⊉ + [5786] = "\u2241", // ≁ + [5792] = "\u2244", // ≄ + [5802] = "\u2247", // ≇ + [5808] = "\u2249", // ≉ + [5820] = "\u2224", // ∤ + [5824] = "\u2226", // ∦ + [5830] = "\u2226", // ∦ + [5833] = "\u2AFD\u20E5", // ⫽⃥ + [5835] = "\u2202\u0338", // ∂̸ + [5841] = "\u2A14", // ⨔ + [5843] = "\u2280", // ⊀ + [5847] = "\u22E0", // ⋠ + [5849] = "\u2AAF\u0338", // ⪯̸ + [5851] = "\u2280", // ⊀ + [5854] = "\u2AAF\u0338", // ⪯̸ + [5859] = "\u21CF", // ⇏ + [5863] = "\u219B", // ↛ + [5865] = "\u2933\u0338", // ⤳̸ + [5867] = "\u219D\u0338", // ↝̸ + [5878] = "\u21CF", // ⇏ + [5888] = "\u219B", // ↛ + [5892] = "\u22EB", // ⋫ + [5894] = "\u22ED", // ⋭ + [5897] = "\u2281", // ⊁ + [5901] = "\u22E1", // ⋡ + [5903] = "\u2AB0\u0338", // ⪰̸ + [5907] = "\uD835\uDCA9", // 𝒩 + [5909] = "\uD835\uDCC3", // 𝓃 + [5917] = "\u2224", // ∤ + [5926] = "\u2226", // ∦ + [5929] = "\u2241", // ≁ + [5931] = "\u2244", // ≄ + [5933] = "\u2244", // ≄ + [5937] = "\u2224", // ∤ + [5941] = "\u2226", // ∦ + [5947] = "\u22E2", // ⋢ + [5950] = "\u22E3", // ⋣ + [5953] = "\u2284", // ⊄ + [5955] = "\u2AC5\u0338", // ⫅̸ + [5957] = "\u2288", // ⊈ + [5961] = "\u2282\u20D2", // ⊂⃒ + [5964] = "\u2288", // ⊈ + [5966] = "\u2AC5\u0338", // ⫅̸ + [5969] = "\u2281", // ⊁ + [5972] = "\u2AB0\u0338", // ⪰̸ + [5974] = "\u2285", // ⊅ + [5976] = "\u2AC6\u0338", // ⫆̸ + [5978] = "\u2289", // ⊉ + [5982] = "\u2283\u20D2", // ⊃⃒ + [5985] = "\u2289", // ⊉ + [5987] = "\u2AC6\u0338", // ⫆̸ + [5991] = "\u2279", // ≹ + [5996] = "\u00D1", // Ñ + [5997] = "\u00D1", // Ñ + [6001] = "\u00F1", // ñ + [6002] = "\u00F1", // ñ + [6005] = "\u2278", // ≸ + [6017] = "\u22EA", // ⋪ + [6020] = "\u22EC", // ⋬ + [6026] = "\u22EB", // ⋫ + [6029] = "\u22ED", // ⋭ + [6031] = "\u039D", // Ν + [6033] = "\u03BD", // ν + [6035] = "\u0023", // # + [6039] = "\u2116", // № + [6042] = "\u2007", //   + [6046] = "\u224D\u20D2", // ≍⃒ + [6052] = "\u22AF", // ⊯ + [6057] = "\u22AE", // ⊮ + [6062] = "\u22AD", // ⊭ + [6067] = "\u22AC", // ⊬ + [6070] = "\u2265\u20D2", // ≥⃒ + [6072] = "\u003E\u20D2", // >⃒ + [6077] = "\u2904", // ⤄ + [6083] = "\u29DE", // ⧞ + [6088] = "\u2902", // ⤂ + [6090] = "\u2264\u20D2", // ≤⃒ + [6092] = "\u003C\u20D2", // <⃒ + [6096] = "\u22B4\u20D2", // ⊴⃒ + [6101] = "\u2903", // ⤃ + [6106] = "\u22B5\u20D2", // ⊵⃒ + [6110] = "\u223C\u20D2", // ∼⃒ + [6116] = "\u2923", // ⤣ + [6120] = "\u21D6", // ⇖ + [6122] = "\u2196", // ↖ + [6125] = "\u2196", // ↖ + [6130] = "\u2927", // ⤧ + [6136] = "\u00D3", // Ó + [6137] = "\u00D3", // Ó + [6143] = "\u00F3", // ó + [6144] = "\u00F3", // ó + [6147] = "\u229B", // ⊛ + [6151] = "\u229A", // ⊚ + [6155] = "\u00D4", // Ô + [6156] = "\u00D4", // Ô + [6157] = "\u00F4", // ô + [6158] = "\u00F4", // ô + [6160] = "\u041E", // О + [6162] = "\u043E", // о + [6167] = "\u229D", // ⊝ + [6173] = "\u0150", // Ő + [6178] = "\u0151", // ő + [6181] = "\u2A38", // ⨸ + [6184] = "\u2299", // ⊙ + [6189] = "\u29BC", // ⦼ + [6194] = "\u0152", // Œ + [6199] = "\u0153", // œ + [6204] = "\u29BF", // ⦿ + [6207] = "\uD835\uDD12", // 𝔒 + [6209] = "\uD835\uDD2C", // 𝔬 + [6213] = "\u02DB", // ˛ + [6218] = "\u00D2", // Ò + [6219] = "\u00D2", // Ò + [6223] = "\u00F2", // ò + [6224] = "\u00F2", // ò + [6226] = "\u29C1", // ⧁ + [6231] = "\u29B5", // ⦵ + [6233] = "\u03A9", // Ω + [6237] = "\u222E", // ∮ + [6242] = "\u21BA", // ↺ + [6246] = "\u29BE", // ⦾ + [6251] = "\u29BB", // ⦻ + [6255] = "\u203E", // ‾ + [6257] = "\u29C0", // ⧀ + [6262] = "\u014C", // Ō + [6267] = "\u014D", // ō + [6271] = "\u03A9", // Ω + [6275] = "\u03C9", // ω + [6281] = "\u039F", // Ο + [6287] = "\u03BF", // ο + [6289] = "\u29B6", // ⦶ + [6293] = "\u2296", // ⊖ + [6297] = "\uD835\uDD46", // 𝕆 + [6301] = "\uD835\uDD60", // 𝕠 + [6305] = "\u29B7", // ⦷ + [6325] = "\u201C", // “ + [6331] = "\u2018", // ‘ + [6335] = "\u29B9", // ⦹ + [6339] = "\u2295", // ⊕ + [6341] = "\u2A54", // ⩔ + [6343] = "\u2228", // ∨ + [6347] = "\u21BB", // ↻ + [6349] = "\u2A5D", // ⩝ + [6352] = "\u2134", // ℴ + [6355] = "\u2134", // ℴ + [6356] = "\u00AA", // ª + [6357] = "\u00AA", // ª + [6358] = "\u00BA", // º + [6359] = "\u00BA", // º + [6364] = "\u22B6", // ⊶ + [6367] = "\u2A56", // ⩖ + [6373] = "\u2A57", // ⩗ + [6375] = "\u2A5B", // ⩛ + [6377] = "\u24C8", // Ⓢ + [6381] = "\uD835\uDCAA", // 𝒪 + [6385] = "\u2134", // ℴ + [6389] = "\u00D8", // Ø + [6390] = "\u00D8", // Ø + [6394] = "\u00F8", // ø + [6395] = "\u00F8", // ø + [6398] = "\u2298", // ⊘ + [6403] = "\u00D5", // Õ + [6404] = "\u00D5", // Õ + [6409] = "\u00F5", // õ + [6410] = "\u00F5", // õ + [6414] = "\u2A37", // ⨷ + [6418] = "\u2297", // ⊗ + [6421] = "\u2A36", // ⨶ + [6424] = "\u00D6", // Ö + [6425] = "\u00D6", // Ö + [6428] = "\u00F6", // ö + [6429] = "\u00F6", // ö + [6434] = "\u233D", // ⌽ + [6441] = "\u203E", // ‾ + [6446] = "\u23DE", // ⏞ + [6450] = "\u23B4", // ⎴ + [6462] = "\u23DC", // ⏜ + [6466] = "\u2225", // ∥ + [6467] = "\u00B6", // ¶ + [6468] = "\u00B6", // ¶ + [6473] = "\u2225", // ∥ + [6477] = "\u2AF3", // ⫳ + [6479] = "\u2AFD", // ⫽ + [6481] = "\u2202", // ∂ + [6490] = "\u2202", // ∂ + [6493] = "\u041F", // П + [6496] = "\u043F", // п + [6502] = "\u0025", // % + [6506] = "\u002E", // . + [6510] = "\u2030", // ‰ + [6512] = "\u22A5", // ⊥ + [6517] = "\u2031", // ‱ + [6520] = "\uD835\uDD13", // 𝔓 + [6523] = "\uD835\uDD2D", // 𝔭 + [6526] = "\u03A6", // Φ + [6529] = "\u03C6", // φ + [6531] = "\u03D5", // ϕ + [6536] = "\u2133", // ℳ + [6540] = "\u260E", // ☎ + [6542] = "\u03A0", // Π + [6544] = "\u03C0", // π + [6552] = "\u22D4", // ⋔ + [6554] = "\u03D6", // ϖ + [6560] = "\u210F", // ℏ + [6562] = "\u210E", // ℎ + [6565] = "\u210F", // ℏ + [6568] = "\u002B", // + + [6573] = "\u2A23", // ⨣ + [6575] = "\u229E", // ⊞ + [6579] = "\u2A22", // ⨢ + [6582] = "\u2214", // ∔ + [6584] = "\u2A25", // ⨥ + [6586] = "\u2A72", // ⩲ + [6595] = "\u00B1", // ± + [6597] = "\u00B1", // ± + [6598] = "\u00B1", // ± + [6602] = "\u2A26", // ⨦ + [6606] = "\u2A27", // ⨧ + [6608] = "\u00B1", // ± + [6621] = "\u210C", // ℌ + [6629] = "\u2A15", // ⨕ + [6632] = "\u2119", // ℙ + [6635] = "\uD835\uDD61", // 𝕡 + [6638] = "\u00A3", // £ + [6639] = "\u00A3", // £ + [6641] = "\u2ABB", // ⪻ + [6643] = "\u227A", // ≺ + [6646] = "\u2AB7", // ⪷ + [6650] = "\u227C", // ≼ + [6652] = "\u2AB3", // ⪳ + [6654] = "\u2AAF", // ⪯ + [6656] = "\u227A", // ≺ + [6663] = "\u2AB7", // ⪷ + [6671] = "\u227C", // ≼ + [6678] = "\u227A", // ≺ + [6684] = "\u2AAF", // ⪯ + [6695] = "\u227C", // ≼ + [6701] = "\u227E", // ≾ + [6704] = "\u2AAF", // ⪯ + [6712] = "\u2AB9", // ⪹ + [6716] = "\u2AB5", // ⪵ + [6720] = "\u22E8", // ⋨ + [6724] = "\u227E", // ≾ + [6728] = "\u2033", // ″ + [6732] = "\u2032", // ′ + [6734] = "\u2119", // ℙ + [6738] = "\u2AB9", // ⪹ + [6740] = "\u2AB5", // ⪵ + [6744] = "\u22E8", // ⋨ + [6747] = "\u220F", // ∏ + [6753] = "\u220F", // ∏ + [6759] = "\u232E", // ⌮ + [6764] = "\u2312", // ⌒ + [6769] = "\u2313", // ⌓ + [6771] = "\u221D", // ∝ + [6779] = "\u2237", // ∷ + [6782] = "\u221D", // ∝ + [6785] = "\u221D", // ∝ + [6789] = "\u227E", // ≾ + [6794] = "\u22B0", // ⊰ + [6798] = "\uD835\uDCAB", // 𝒫 + [6802] = "\uD835\uDCC5", // 𝓅 + [6804] = "\u03A8", // Ψ + [6806] = "\u03C8", // ψ + [6812] = "\u2008", //   + [6816] = "\uD835\uDD14", // 𝔔 + [6820] = "\uD835\uDD2E", // 𝔮 + [6824] = "\u2A0C", // ⨌ + [6828] = "\u211A", // ℚ + [6832] = "\uD835\uDD62", // 𝕢 + [6838] = "\u2057", // ⁗ + [6842] = "\uD835\uDCAC", // 𝒬 + [6846] = "\uD835\uDCC6", // 𝓆 + [6857] = "\u210D", // ℍ + [6861] = "\u2A16", // ⨖ + [6865] = "\u003F", // ? + [6868] = "\u225F", // ≟ + [6871] = "\u0022", // " + [6872] = "\u0022", // " + [6874] = "\u0022", // " + [6875] = "\u0022", // " + [6881] = "\u21DB", // ⇛ + [6885] = "\u223D\u0331", // ∽̱ + [6892] = "\u0154", // Ŕ + [6896] = "\u0155", // ŕ + [6900] = "\u221A", // √ + [6907] = "\u29B3", // ⦳ + [6910] = "\u27EB", // ⟫ + [6913] = "\u27E9", // ⟩ + [6915] = "\u2992", // ⦒ + [6917] = "\u29A5", // ⦥ + [6920] = "\u27E9", // ⟩ + [6923] = "\u00BB", // » + [6924] = "\u00BB", // » + [6927] = "\u21A0", // ↠ + [6930] = "\u21D2", // ⇒ + [6933] = "\u2192", // → + [6936] = "\u2975", // ⥵ + [6938] = "\u21E5", // ⇥ + [6941] = "\u2920", // ⤠ + [6943] = "\u2933", // ⤳ + [6946] = "\u291E", // ⤞ + [6949] = "\u21AA", // ↪ + [6952] = "\u21AC", // ↬ + [6955] = "\u2945", // ⥅ + [6959] = "\u2974", // ⥴ + [6962] = "\u2916", // ⤖ + [6965] = "\u21A3", // ↣ + [6967] = "\u219D", // ↝ + [6972] = "\u291C", // ⤜ + [6977] = "\u291A", // ⤚ + [6980] = "\u2236", // ∶ + [6985] = "\u211A", // ℚ + [6990] = "\u2910", // ⤐ + [6995] = "\u290F", // ⤏ + [7000] = "\u290D", // ⤍ + [7004] = "\u2773", // ❳ + [7009] = "\u007D", // } + [7011] = "\u005D", // ] + [7014] = "\u298C", // ⦌ + [7018] = "\u298E", // ⦎ + [7020] = "\u2990", // ⦐ + [7026] = "\u0158", // Ř + [7032] = "\u0159", // ř + [7037] = "\u0156", // Ŗ + [7042] = "\u0157", // ŗ + [7045] = "\u2309", // ⌉ + [7048] = "\u007D", // } + [7050] = "\u0420", // Р + [7052] = "\u0440", // р + [7056] = "\u2937", // ⤷ + [7062] = "\u2969", // ⥩ + [7066] = "\u201D", // ” + [7068] = "\u201D", // ” + [7071] = "\u21B3", // ↳ + [7073] = "\u211C", // ℜ + [7077] = "\u211C", // ℜ + [7081] = "\u211B", // ℛ + [7086] = "\u211C", // ℜ + [7088] = "\u211D", // ℝ + [7091] = "\u25AD", // ▭ + [7093] = "\u00AE", // ® + [7094] = "\u00AE", // ® + [7095] = "\u00AE", // ® + [7096] = "\u00AE", // ® + [7109] = "\u220B", // ∋ + [7120] = "\u21CB", // ⇋ + [7134] = "\u296F", // ⥯ + [7140] = "\u297D", // ⥽ + [7145] = "\u230B", // ⌋ + [7148] = "\u211C", // ℜ + [7150] = "\uD835\uDD2F", // 𝔯 + [7154] = "\u2964", // ⥤ + [7159] = "\u21C1", // ⇁ + [7161] = "\u21C0", // ⇀ + [7163] = "\u296C", // ⥬ + [7166] = "\u03A1", // Ρ + [7168] = "\u03C1", // ρ + [7170] = "\u03F1", // ϱ + [7187] = "\u27E9", // ⟩ + [7192] = "\u2192", // → + [7198] = "\u21D2", // ⇒ + [7208] = "\u2192", // → + [7212] = "\u21E5", // ⇥ + [7222] = "\u21C4", // ⇄ + [7227] = "\u21A3", // ↣ + [7235] = "\u2309", // ⌉ + [7249] = "\u27E7", // ⟧ + [7261] = "\u295D", // ⥝ + [7268] = "\u21C2", // ⇂ + [7272] = "\u2955", // ⥕ + [7278] = "\u230B", // ⌋ + [7290] = "\u21C1", // ⇁ + [7293] = "\u21C0", // ⇀ + [7304] = "\u21C4", // ⇄ + [7313] = "\u21CC", // ⇌ + [7325] = "\u21C9", // ⇉ + [7336] = "\u219D", // ↝ + [7340] = "\u22A2", // ⊢ + [7346] = "\u21A6", // ↦ + [7353] = "\u295B", // ⥛ + [7364] = "\u22CC", // ⋌ + [7372] = "\u22B3", // ⊳ + [7376] = "\u29D0", // ⧐ + [7382] = "\u22B5", // ⊵ + [7395] = "\u294F", // ⥏ + [7405] = "\u295C", // ⥜ + [7412] = "\u21BE", // ↾ + [7416] = "\u2954", // ⥔ + [7423] = "\u21C0", // ⇀ + [7427] = "\u2953", // ⥓ + [7430] = "\u02DA", // ˚ + [7441] = "\u2253", // ≓ + [7446] = "\u21C4", // ⇄ + [7450] = "\u21CC", // ⇌ + [7452] = "\u200F", // ‏ + [7458] = "\u23B1", // ⎱ + [7463] = "\u23B1", // ⎱ + [7468] = "\u2AEE", // ⫮ + [7473] = "\u27ED", // ⟭ + [7476] = "\u21FE", // ⇾ + [7480] = "\u27E7", // ⟧ + [7484] = "\u2986", // ⦆ + [7488] = "\u211D", // ℝ + [7490] = "\uD835\uDD63", // 𝕣 + [7494] = "\u2A2E", // ⨮ + [7500] = "\u2A35", // ⨵ + [7511] = "\u2970", // ⥰ + [7515] = "\u0029", // ) + [7518] = "\u2994", // ⦔ + [7525] = "\u2A12", // ⨒ + [7530] = "\u21C9", // ⇉ + [7541] = "\u21DB", // ⇛ + [7547] = "\u203A", // › + [7551] = "\u211B", // ℛ + [7554] = "\uD835\uDCC7", // 𝓇 + [7556] = "\u21B1", // ↱ + [7558] = "\u21B1", // ↱ + [7561] = "\u005D", // ] + [7564] = "\u2019", // ’ + [7566] = "\u2019", // ’ + [7572] = "\u22CC", // ⋌ + [7577] = "\u22CA", // ⋊ + [7580] = "\u25B9", // ▹ + [7582] = "\u22B5", // ⊵ + [7584] = "\u25B8", // ▸ + [7589] = "\u29CE", // ⧎ + [7600] = "\u29F4", // ⧴ + [7607] = "\u2968", // ⥨ + [7609] = "\u211E", // ℞ + [7616] = "\u015A", // Ś + [7623] = "\u015B", // ś + [7628] = "\u201A", // ‚ + [7630] = "\u2ABC", // ⪼ + [7632] = "\u227B", // ≻ + [7635] = "\u2AB8", // ⪸ + [7640] = "\u0160", // Š + [7644] = "\u0161", // š + [7648] = "\u227D", // ≽ + [7650] = "\u2AB4", // ⪴ + [7652] = "\u2AB0", // ⪰ + [7657] = "\u015E", // Ş + [7661] = "\u015F", // ş + [7665] = "\u015C", // Ŝ + [7669] = "\u015D", // ŝ + [7673] = "\u2ABA", // ⪺ + [7675] = "\u2AB6", // ⪶ + [7679] = "\u22E9", // ⋩ + [7686] = "\u2A13", // ⨓ + [7690] = "\u227F", // ≿ + [7692] = "\u0421", // С + [7694] = "\u0441", // с + [7698] = "\u22C5", // ⋅ + [7700] = "\u22A1", // ⊡ + [7702] = "\u2A66", // ⩦ + [7708] = "\u2925", // ⤥ + [7712] = "\u21D8", // ⇘ + [7714] = "\u2198", // ↘ + [7717] = "\u2198", // ↘ + [7719] = "\u00A7", // § + [7720] = "\u00A7", // § + [7723] = "\u003B", // ; + [7728] = "\u2929", // ⤩ + [7735] = "\u2216", // ∖ + [7737] = "\u2216", // ∖ + [7740] = "\u2736", // ✶ + [7743] = "\uD835\uDD16", // 𝔖 + [7746] = "\uD835\uDD30", // 𝔰 + [7750] = "\u2322", // ⌢ + [7755] = "\u266F", // ♯ + [7761] = "\u0429", // Щ + [7766] = "\u0449", // щ + [7769] = "\u0428", // Ш + [7771] = "\u0448", // ш + [7785] = "\u2193", // ↓ + [7795] = "\u2190", // ← + [7802] = "\u2223", // ∣ + [7811] = "\u2225", // ∥ + [7822] = "\u2192", // → + [7830] = "\u2191", // ↑ + [7831] = "\u00AD", // ­ + [7832] = "\u00AD", // ­ + [7837] = "\u03A3", // Σ + [7842] = "\u03C3", // σ + [7844] = "\u03C2", // ς + [7846] = "\u03C2", // ς + [7848] = "\u223C", // ∼ + [7852] = "\u2A6A", // ⩪ + [7854] = "\u2243", // ≃ + [7856] = "\u2243", // ≃ + [7858] = "\u2A9E", // ⪞ + [7860] = "\u2AA0", // ⪠ + [7862] = "\u2A9D", // ⪝ + [7864] = "\u2A9F", // ⪟ + [7867] = "\u2246", // ≆ + [7872] = "\u2A24", // ⨤ + [7877] = "\u2972", // ⥲ + [7882] = "\u2190", // ← + [7893] = "\u2218", // ∘ + [7906] = "\u2216", // ∖ + [7910] = "\u2A33", // ⨳ + [7917] = "\u29E4", // ⧤ + [7920] = "\u2223", // ∣ + [7923] = "\u2323", // ⌣ + [7925] = "\u2AAA", // ⪪ + [7927] = "\u2AAC", // ⪬ + [7929] = "\u2AAC\uFE00", // ⪬︀ + [7935] = "\u042C", // Ь + [7941] = "\u044C", // ь + [7943] = "\u002F", // / + [7945] = "\u29C4", // ⧄ + [7948] = "\u233F", // ⌿ + [7952] = "\uD835\uDD4A", // 𝕊 + [7955] = "\uD835\uDD64", // 𝕤 + [7961] = "\u2660", // ♠ + [7965] = "\u2660", // ♠ + [7967] = "\u2225", // ∥ + [7972] = "\u2293", // ⊓ + [7974] = "\u2293\uFE00", // ⊓︀ + [7977] = "\u2294", // ⊔ + [7979] = "\u2294\uFE00", // ⊔︀ + [7983] = "\u221A", // √ + [7987] = "\u228F", // ⊏ + [7989] = "\u2291", // ⊑ + [7993] = "\u228F", // ⊏ + [7996] = "\u2291", // ⊑ + [7998] = "\u2290", // ⊐ + [8000] = "\u2292", // ⊒ + [8004] = "\u2290", // ⊐ + [8007] = "\u2292", // ⊒ + [8009] = "\u25A1", // □ + [8014] = "\u25A1", // □ + [8018] = "\u25A1", // □ + [8031] = "\u2293", // ⊓ + [8038] = "\u228F", // ⊏ + [8044] = "\u2291", // ⊑ + [8051] = "\u2290", // ⊐ + [8057] = "\u2292", // ⊒ + [8063] = "\u2294", // ⊔ + [8065] = "\u25AA", // ▪ + [8067] = "\u25AA", // ▪ + [8072] = "\u2192", // → + [8076] = "\uD835\uDCAE", // 𝒮 + [8080] = "\uD835\uDCC8", // 𝓈 + [8085] = "\u2216", // ∖ + [8090] = "\u2323", // ⌣ + [8095] = "\u22C6", // ⋆ + [8099] = "\u22C6", // ⋆ + [8103] = "\u2606", // ☆ + [8105] = "\u2605", // ★ + [8119] = "\u03F5", // ϵ + [8123] = "\u03D5", // ϕ + [8126] = "\u00AF", // ¯ + [8129] = "\u22D0", // ⋐ + [8132] = "\u2282", // ⊂ + [8136] = "\u2ABD", // ⪽ + [8138] = "\u2AC5", // ⫅ + [8140] = "\u2286", // ⊆ + [8144] = "\u2AC3", // ⫃ + [8149] = "\u2AC1", // ⫁ + [8152] = "\u2ACB", // ⫋ + [8154] = "\u228A", // ⊊ + [8159] = "\u2ABF", // ⪿ + [8164] = "\u2979", // ⥹ + [8168] = "\u22D0", // ⋐ + [8172] = "\u2282", // ⊂ + [8175] = "\u2286", // ⊆ + [8177] = "\u2AC5", // ⫅ + [8183] = "\u2286", // ⊆ + [8187] = "\u228A", // ⊊ + [8189] = "\u2ACB", // ⫋ + [8192] = "\u2AC7", // ⫇ + [8195] = "\u2AD5", // ⫕ + [8197] = "\u2AD3", // ⫓ + [8200] = "\u227B", // ≻ + [8207] = "\u2AB8", // ⪸ + [8215] = "\u227D", // ≽ + [8222] = "\u227B", // ≻ + [8228] = "\u2AB0", // ⪰ + [8239] = "\u227D", // ≽ + [8245] = "\u227F", // ≿ + [8248] = "\u2AB0", // ⪰ + [8256] = "\u2ABA", // ⪺ + [8260] = "\u2AB6", // ⪶ + [8264] = "\u22E9", // ⋩ + [8268] = "\u227F", // ≿ + [8274] = "\u220B", // ∋ + [8276] = "\u2211", // ∑ + [8278] = "\u2211", // ∑ + [8281] = "\u266A", // ♪ + [8283] = "\u22D1", // ⋑ + [8285] = "\u2283", // ⊃ + [8286] = "\u00B9", // ¹ + [8287] = "\u00B9", // ¹ + [8288] = "\u00B2", // ² + [8289] = "\u00B2", // ² + [8290] = "\u00B3", // ³ + [8291] = "\u00B3", // ³ + [8295] = "\u2ABE", // ⪾ + [8299] = "\u2AD8", // ⫘ + [8301] = "\u2AC6", // ⫆ + [8303] = "\u2287", // ⊇ + [8307] = "\u2AC4", // ⫄ + [8313] = "\u2283", // ⊃ + [8319] = "\u2287", // ⊇ + [8324] = "\u27C9", // ⟉ + [8327] = "\u2AD7", // ⫗ + [8332] = "\u297B", // ⥻ + [8337] = "\u2AC2", // ⫂ + [8340] = "\u2ACC", // ⫌ + [8342] = "\u228B", // ⊋ + [8347] = "\u2AC0", // ⫀ + [8351] = "\u22D1", // ⋑ + [8355] = "\u2283", // ⊃ + [8358] = "\u2287", // ⊇ + [8360] = "\u2AC6", // ⫆ + [8364] = "\u228B", // ⊋ + [8366] = "\u2ACC", // ⫌ + [8369] = "\u2AC8", // ⫈ + [8372] = "\u2AD4", // ⫔ + [8374] = "\u2AD6", // ⫖ + [8380] = "\u2926", // ⤦ + [8384] = "\u21D9", // ⇙ + [8386] = "\u2199", // ↙ + [8389] = "\u2199", // ↙ + [8394] = "\u292A", // ⤪ + [8398] = "\u00DF", // ß + [8399] = "\u00DF", // ß + [8403] = "\u0009", // + [8410] = "\u2316", // ⌖ + [8412] = "\u03A4", // Τ + [8414] = "\u03C4", // τ + [8418] = "\u23B4", // ⎴ + [8424] = "\u0164", // Ť + [8430] = "\u0165", // ť + [8435] = "\u0162", // Ţ + [8440] = "\u0163", // ţ + [8442] = "\u0422", // Т + [8444] = "\u0442", // т + [8448] = "\u20DB", // ⃛ + [8454] = "\u2315", // ⌕ + [8457] = "\uD835\uDD17", // 𝔗 + [8460] = "\uD835\uDD31", // 𝔱 + [8466] = "\u2234", // ∴ + [8475] = "\u2234", // ∴ + [8480] = "\u2234", // ∴ + [8483] = "\u0398", // Θ + [8486] = "\u03B8", // θ + [8490] = "\u03D1", // ϑ + [8492] = "\u03D1", // ϑ + [8502] = "\u2248", // ≈ + [8506] = "\u223C", // ∼ + [8515] = "\u205F\u200A", //    + [8519] = "\u2009", //   + [8526] = "\u2009", //   + [8530] = "\u2248", // ≈ + [8534] = "\u223C", // ∼ + [8538] = "\u00DE", // Þ + [8539] = "\u00DE", // Þ + [8542] = "\u00FE", // þ + [8543] = "\u00FE", // þ + [8548] = "\u223C", // ∼ + [8553] = "\u02DC", // ˜ + [8559] = "\u2243", // ≃ + [8569] = "\u2245", // ≅ + [8575] = "\u2248", // ≈ + [8578] = "\u00D7", // × + [8579] = "\u00D7", // × + [8581] = "\u22A0", // ⊠ + [8584] = "\u2A31", // ⨱ + [8586] = "\u2A30", // ⨰ + [8589] = "\u222D", // ∭ + [8593] = "\u2928", // ⤨ + [8595] = "\u22A4", // ⊤ + [8599] = "\u2336", // ⌶ + [8603] = "\u2AF1", // ⫱ + [8607] = "\uD835\uDD4B", // 𝕋 + [8609] = "\uD835\uDD65", // 𝕥 + [8613] = "\u2ADA", // ⫚ + [8616] = "\u2929", // ⤩ + [8622] = "\u2034", // ‴ + [8627] = "\u2122", // ™ + [8632] = "\u2122", // ™ + [8639] = "\u25B5", // ▵ + [8644] = "\u25BF", // ▿ + [8649] = "\u25C3", // ◃ + [8652] = "\u22B4", // ⊴ + [8654] = "\u225C", // ≜ + [8660] = "\u25B9", // ▹ + [8663] = "\u22B5", // ⊵ + [8667] = "\u25EC", // ◬ + [8669] = "\u225C", // ≜ + [8675] = "\u2A3A", // ⨺ + [8684] = "\u20DB", // ⃛ + [8689] = "\u2A39", // ⨹ + [8692] = "\u29CD", // ⧍ + [8697] = "\u2A3B", // ⨻ + [8704] = "\u23E2", // ⏢ + [8708] = "\uD835\uDCAF", // 𝒯 + [8712] = "\uD835\uDCC9", // 𝓉 + [8716] = "\u0426", // Ц + [8718] = "\u0446", // ц + [8722] = "\u040B", // Ћ + [8726] = "\u045B", // ћ + [8731] = "\u0166", // Ŧ + [8736] = "\u0167", // ŧ + [8741] = "\u226C", // ≬ + [8756] = "\u219E", // ↞ + [8767] = "\u21A0", // ↠ + [8773] = "\u00DA", // Ú + [8774] = "\u00DA", // Ú + [8780] = "\u00FA", // ú + [8781] = "\u00FA", // ú + [8784] = "\u219F", // ↟ + [8788] = "\u21D1", // ⇑ + [8791] = "\u2191", // ↑ + [8796] = "\u2949", // ⥉ + [8801] = "\u040E", // Ў + [8806] = "\u045E", // ў + [8810] = "\u016C", // Ŭ + [8814] = "\u016D", // ŭ + [8818] = "\u00DB", // Û + [8819] = "\u00DB", // Û + [8823] = "\u00FB", // û + [8824] = "\u00FB", // û + [8826] = "\u0423", // У + [8828] = "\u0443", // у + [8833] = "\u21C5", // ⇅ + [8839] = "\u0170", // Ű + [8844] = "\u0171", // ű + [8848] = "\u296E", // ⥮ + [8854] = "\u297E", // ⥾ + [8857] = "\uD835\uDD18", // 𝔘 + [8859] = "\uD835\uDD32", // 𝔲 + [8864] = "\u00D9", // Ù + [8865] = "\u00D9", // Ù + [8870] = "\u00F9", // ù + [8871] = "\u00F9", // ù + [8875] = "\u2963", // ⥣ + [8880] = "\u21BF", // ↿ + [8882] = "\u21BE", // ↾ + [8886] = "\u2580", // ▀ + [8892] = "\u231C", // ⌜ + [8895] = "\u231C", // ⌜ + [8899] = "\u230F", // ⌏ + [8903] = "\u25F8", // ◸ + [8908] = "\u016A", // Ū + [8913] = "\u016B", // ū + [8914] = "\u00A8", // ¨ + [8915] = "\u00A8", // ¨ + [8923] = "\u005F", // _ + [8928] = "\u23DF", // ⏟ + [8932] = "\u23B5", // ⎵ + [8944] = "\u23DD", // ⏝ + [8948] = "\u22C3", // ⋃ + [8953] = "\u228E", // ⊎ + [8958] = "\u0172", // Ų + [8963] = "\u0173", // ų + [8966] = "\uD835\uDD4C", // 𝕌 + [8969] = "\uD835\uDD66", // 𝕦 + [8976] = "\u2191", // ↑ + [8982] = "\u21D1", // ⇑ + [8989] = "\u2191", // ↑ + [8993] = "\u2912", // ⤒ + [9003] = "\u21C5", // ⇅ + [9013] = "\u2195", // ↕ + [9023] = "\u21D5", // ⇕ + [9033] = "\u2195", // ↕ + [9045] = "\u296E", // ⥮ + [9057] = "\u21BF", // ↿ + [9063] = "\u21BE", // ↾ + [9067] = "\u228E", // ⊎ + [9080] = "\u2196", // ↖ + [9091] = "\u2197", // ↗ + [9094] = "\u03D2", // ϒ + [9097] = "\u03C5", // υ + [9099] = "\u03D2", // ϒ + [9103] = "\u03A5", // Υ + [9107] = "\u03C5", // υ + [9111] = "\u22A5", // ⊥ + [9117] = "\u21A5", // ↥ + [9126] = "\u21C8", // ⇈ + [9132] = "\u231D", // ⌝ + [9135] = "\u231D", // ⌝ + [9139] = "\u230E", // ⌎ + [9144] = "\u016E", // Ů + [9148] = "\u016F", // ů + [9152] = "\u25F9", // ◹ + [9156] = "\uD835\uDCB0", // 𝒰 + [9160] = "\uD835\uDCCA", // 𝓊 + [9165] = "\u22F0", // ⋰ + [9171] = "\u0168", // Ũ + [9176] = "\u0169", // ũ + [9179] = "\u25B5", // ▵ + [9181] = "\u25B4", // ▴ + [9186] = "\u21C8", // ⇈ + [9189] = "\u00DC", // Ü + [9190] = "\u00DC", // Ü + [9192] = "\u00FC", // ü + [9193] = "\u00FC", // ü + [9200] = "\u29A7", // ⦧ + [9207] = "\u299C", // ⦜ + [9216] = "\u03F5", // ϵ + [9222] = "\u03F0", // ϰ + [9230] = "\u2205", // ∅ + [9234] = "\u03D5", // ϕ + [9236] = "\u03D6", // ϖ + [9242] = "\u221D", // ∝ + [9246] = "\u21D5", // ⇕ + [9248] = "\u2195", // ↕ + [9251] = "\u03F1", // ϱ + [9257] = "\u03C2", // ς + [9266] = "\u228A\uFE00", // ⊊︀ + [9268] = "\u2ACB\uFE00", // ⫋︀ + [9276] = "\u228B\uFE00", // ⊋︀ + [9278] = "\u2ACC\uFE00", // ⫌︀ + [9284] = "\u03D1", // ϑ + [9296] = "\u22B2", // ⊲ + [9302] = "\u22B3", // ⊳ + [9307] = "\u2AEB", // ⫫ + [9311] = "\u2AE8", // ⫨ + [9313] = "\u2AE9", // ⫩ + [9316] = "\u0412", // В + [9319] = "\u0432", // в + [9324] = "\u22AB", // ⊫ + [9329] = "\u22A9", // ⊩ + [9334] = "\u22A8", // ⊨ + [9339] = "\u22A2", // ⊢ + [9341] = "\u2AE6", // ⫦ + [9344] = "\u22C1", // ⋁ + [9347] = "\u2228", // ∨ + [9351] = "\u22BB", // ⊻ + [9354] = "\u225A", // ≚ + [9359] = "\u22EE", // ⋮ + [9364] = "\u2016", // ‖ + [9369] = "\u007C", // | + [9371] = "\u2016", // ‖ + [9373] = "\u007C", // | + [9381] = "\u2223", // ∣ + [9386] = "\u007C", // | + [9396] = "\u2758", // ❘ + [9402] = "\u2240", // ≀ + [9413] = "\u200A", //   + [9416] = "\uD835\uDD19", // 𝔙 + [9419] = "\uD835\uDD33", // 𝔳 + [9424] = "\u22B2", // ⊲ + [9429] = "\u2282\u20D2", // ⊂⃒ + [9431] = "\u2283\u20D2", // ⊃⃒ + [9435] = "\uD835\uDD4D", // 𝕍 + [9439] = "\uD835\uDD67", // 𝕧 + [9444] = "\u221D", // ∝ + [9449] = "\u22B3", // ⊳ + [9453] = "\uD835\uDCB1", // 𝒱 + [9457] = "\uD835\uDCCB", // 𝓋 + [9462] = "\u2ACB\uFE00", // ⫋︀ + [9464] = "\u228A\uFE00", // ⊊︀ + [9468] = "\u2ACC\uFE00", // ⫌︀ + [9470] = "\u228B\uFE00", // ⊋︀ + [9476] = "\u22AA", // ⊪ + [9483] = "\u299A", // ⦚ + [9489] = "\u0174", // Ŵ + [9495] = "\u0175", // ŵ + [9501] = "\u2A5F", // ⩟ + [9506] = "\u22C0", // ⋀ + [9509] = "\u2227", // ∧ + [9511] = "\u2259", // ≙ + [9516] = "\u2118", // ℘ + [9519] = "\uD835\uDD1A", // 𝔚 + [9522] = "\uD835\uDD34", // 𝔴 + [9526] = "\uD835\uDD4E", // 𝕎 + [9530] = "\uD835\uDD68", // 𝕨 + [9532] = "\u2118", // ℘ + [9534] = "\u2240", // ≀ + [9539] = "\u2240", // ≀ + [9543] = "\uD835\uDCB2", // 𝒲 + [9547] = "\uD835\uDCCC", // 𝓌 + [9552] = "\u22C2", // ⋂ + [9556] = "\u25EF", // ◯ + [9559] = "\u22C3", // ⋃ + [9564] = "\u25BD", // ▽ + [9568] = "\uD835\uDD1B", // 𝔛 + [9571] = "\uD835\uDD35", // 𝔵 + [9576] = "\u27FA", // ⟺ + [9580] = "\u27F7", // ⟷ + [9582] = "\u039E", // Ξ + [9584] = "\u03BE", // ξ + [9589] = "\u27F8", // ⟸ + [9593] = "\u27F5", // ⟵ + [9597] = "\u27FC", // ⟼ + [9601] = "\u22FB", // ⋻ + [9606] = "\u2A00", // ⨀ + [9610] = "\uD835\uDD4F", // 𝕏 + [9613] = "\uD835\uDD69", // 𝕩 + [9617] = "\u2A01", // ⨁ + [9622] = "\u2A02", // ⨂ + [9627] = "\u27F9", // ⟹ + [9631] = "\u27F6", // ⟶ + [9635] = "\uD835\uDCB3", // 𝒳 + [9639] = "\uD835\uDCCD", // 𝓍 + [9644] = "\u2A06", // ⨆ + [9650] = "\u2A04", // ⨄ + [9654] = "\u25B3", // △ + [9658] = "\u22C1", // ⋁ + [9664] = "\u22C0", // ⋀ + [9670] = "\u00DD", // Ý + [9671] = "\u00DD", // Ý + [9677] = "\u00FD", // ý + [9678] = "\u00FD", // ý + [9682] = "\u042F", // Я + [9684] = "\u044F", // я + [9689] = "\u0176", // Ŷ + [9694] = "\u0177", // ŷ + [9696] = "\u042B", // Ы + [9698] = "\u044B", // ы + [9700] = "\u00A5", // ¥ + [9701] = "\u00A5", // ¥ + [9704] = "\uD835\uDD1C", // 𝔜 + [9707] = "\uD835\uDD36", // 𝔶 + [9711] = "\u0407", // Ї + [9715] = "\u0457", // ї + [9719] = "\uD835\uDD50", // 𝕐 + [9723] = "\uD835\uDD6A", // 𝕪 + [9727] = "\uD835\uDCB4", // 𝒴 + [9731] = "\uD835\uDCCE", // 𝓎 + [9735] = "\u042E", // Ю + [9739] = "\u044E", // ю + [9743] = "\u0178", // Ÿ + [9745] = "\u00FF", // ÿ + [9746] = "\u00FF", // ÿ + [9753] = "\u0179", // Ź + [9760] = "\u017A", // ź + [9766] = "\u017D", // Ž + [9772] = "\u017E", // ž + [9774] = "\u0417", // З + [9776] = "\u0437", // з + [9780] = "\u017B", // Ż + [9784] = "\u017C", // ż + [9790] = "\u2128", // ℨ + [9804] = "\u200B", // ​ + [9807] = "\u0396", // Ζ + [9810] = "\u03B6", // ζ + [9813] = "\u2128", // ℨ + [9816] = "\uD835\uDD37", // 𝔷 + [9820] = "\u0416", // Ж + [9824] = "\u0436", // ж + [9831] = "\u21DD", // ⇝ + [9835] = "\u2124", // ℤ + [9839] = "\uD835\uDD6B", // 𝕫 + [9843] = "\uD835\uDCB5", // 𝒵 + [9847] = "\uD835\uDCCF", // 𝓏 + [9850] = "\u200D", // ‍ + [9853] = "\u200C", // ‌ + }; + } + + static int BinarySearchNextState (Transition[] transitions, int state) + { + int min = 0, max = transitions.Length; + + do { + int i = min + ((max - min) / 2); + + if (state > transitions[i].From) { + min = i + 1; + } else if (state < transitions[i].From) { + max = i; + } else { + return transitions[i].To; + } + } while (min < max); + + return -1; + } + + bool PushNamedEntity (char c) + { + int next, state = states[index - 1]; + Transition[] table = null; + + switch (c) { + case '1': table = TransitionTable_1; break; + case '2': table = TransitionTable_2; break; + case '3': table = TransitionTable_3; break; + case '4': table = TransitionTable_4; break; + case '5': table = TransitionTable_5; break; + case '6': table = TransitionTable_6; break; + case '7': table = TransitionTable_7; break; + case '8': table = TransitionTable_8; break; + case ';': table = TransitionTable_semicolon; break; + case 'A': table = TransitionTable_A; break; + case 'B': table = TransitionTable_B; break; + case 'C': table = TransitionTable_C; break; + case 'D': table = TransitionTable_D; break; + case 'E': table = TransitionTable_E; break; + case 'F': table = TransitionTable_F; break; + case 'G': table = TransitionTable_G; break; + case 'H': table = TransitionTable_H; break; + case 'I': table = TransitionTable_I; break; + case 'J': table = TransitionTable_J; break; + case 'K': table = TransitionTable_K; break; + case 'L': table = TransitionTable_L; break; + case 'M': table = TransitionTable_M; break; + case 'N': table = TransitionTable_N; break; + case 'O': table = TransitionTable_O; break; + case 'P': table = TransitionTable_P; break; + case 'Q': table = TransitionTable_Q; break; + case 'R': table = TransitionTable_R; break; + case 'S': table = TransitionTable_S; break; + case 'T': table = TransitionTable_T; break; + case 'U': table = TransitionTable_U; break; + case 'V': table = TransitionTable_V; break; + case 'W': table = TransitionTable_W; break; + case 'X': table = TransitionTable_X; break; + case 'Y': table = TransitionTable_Y; break; + case 'Z': table = TransitionTable_Z; break; + case 'a': table = TransitionTable_a; break; + case 'b': table = TransitionTable_b; break; + case 'c': table = TransitionTable_c; break; + case 'd': table = TransitionTable_d; break; + case 'e': table = TransitionTable_e; break; + case 'f': table = TransitionTable_f; break; + case 'g': table = TransitionTable_g; break; + case 'h': table = TransitionTable_h; break; + case 'i': table = TransitionTable_i; break; + case 'j': table = TransitionTable_j; break; + case 'k': table = TransitionTable_k; break; + case 'l': table = TransitionTable_l; break; + case 'm': table = TransitionTable_m; break; + case 'n': table = TransitionTable_n; break; + case 'o': table = TransitionTable_o; break; + case 'p': table = TransitionTable_p; break; + case 'q': table = TransitionTable_q; break; + case 'r': table = TransitionTable_r; break; + case 's': table = TransitionTable_s; break; + case 't': table = TransitionTable_t; break; + case 'u': table = TransitionTable_u; break; + case 'v': table = TransitionTable_v; break; + case 'w': table = TransitionTable_w; break; + case 'x': table = TransitionTable_x; break; + case 'y': table = TransitionTable_y; break; + case 'z': table = TransitionTable_z; break; + default: return false; + } + + if ((next = BinarySearchNextState (table, state)) == -1) + return false; + + states[index] = next; + pushed[index] = c; + index++; + + return true; + } + + string GetNamedEntityValue () + { + int startIndex = index; + string decoded = null; + + while (startIndex > 0) { + if (NamedEntities.TryGetValue (states[startIndex - 1], out decoded)) + break; + + startIndex--; + } + + if (decoded == null) + decoded = string.Empty; + + if (startIndex < index) + decoded += new string (pushed, startIndex, index - startIndex); + + return decoded; + } + } +} diff --git a/src/MimeKit/Text/HtmlNamespace.cs b/src/MimeKit/Text/HtmlNamespace.cs new file mode 100644 index 0000000..ee4d935 --- /dev/null +++ b/src/MimeKit/Text/HtmlNamespace.cs @@ -0,0 +1,133 @@ +// +// HtmlNamespace.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Text { + /// + /// An HTML namespace. + /// + /// + /// An HTML namespace. + /// + public enum HtmlNamespace { + /// + /// The namespace is "http://www.w3.org/1999/xhtml". + /// + Html, + + /// + /// The namespace is "http://www.w3.org/1998/Math/MathML". + /// + MathML, + + /// + /// The namespace is "http://www.w3.org/2000/svg". + /// + Svg, + + /// + /// The namespace is "http://www.w3.org/1999/xlink". + /// + XLink, + + /// + /// The namespace is "http://www.w3.org/XML/1998/namespace". + /// + Xml, + + /// + /// The namespace is "http://www.w3.org/2000/xmlns/". + /// + XmlNS + } + + /// + /// extension methods. + /// + /// + /// extension methods. + /// + static class HtmlNamespaceExtensions + { + static readonly int NamespacePrefixLength = "http://www.w3.org/".Length; + + static readonly string[] NamespaceValues = { + "http://www.w3.org/1999/xhtml", + "http://www.w3.org/1998/Math/MathML", + "http://www.w3.org/2000/svg", + "http://www.w3.org/1999/xlink", + "http://www.w3.org/XML/1998/namespace", + "http://www.w3.org/2000/xmlns/" + }; + + /// + /// Converts the enum value into the equivalent namespace url. + /// + /// + /// Converts the enum value into the equivalent namespace url. + /// + /// The tag name. + /// The enum value. + public static string ToNamespaceUrl (this HtmlNamespace value) + { + int index = (int) value; + + if (index < 0 || index >= NamespaceValues.Length) + throw new ArgumentOutOfRangeException (nameof (value)); + + return NamespaceValues[index]; + } + + /// + /// Converts the tag name into the equivalent tag id. + /// + /// + /// Converts the tag name into the equivalent tag id. + /// + /// The tag id. + /// The namespace. + public static HtmlNamespace ToHtmlNamespace (this string ns) + { + if (ns == null) + throw new ArgumentNullException (nameof (ns)); + + if (!ns.StartsWith ("http://www.w3.org/", StringComparison.OrdinalIgnoreCase)) + return HtmlNamespace.Html; + + for (int i = 0; i < NamespaceValues.Length; i++) { + if (ns.Length != NamespaceValues[i].Length) + continue; + + if (string.Compare (ns, NamespacePrefixLength, NamespaceValues[i], NamespacePrefixLength, + ns.Length - NamespacePrefixLength, StringComparison.OrdinalIgnoreCase) == 0) + return (HtmlNamespace) i; + } + + return HtmlNamespace.Html; + } + } +} diff --git a/src/MimeKit/Text/HtmlTagCallback.cs b/src/MimeKit/Text/HtmlTagCallback.cs new file mode 100644 index 0000000..61deac1 --- /dev/null +++ b/src/MimeKit/Text/HtmlTagCallback.cs @@ -0,0 +1,42 @@ +// +// HtmlTagCallback.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Text { + /// + /// An HTML tag callback delegate. + /// + /// + /// The delegate is called when a converter + /// is ready to write a new HTML tag, allowing developers to customize + /// whether the tag gets written at all, which attributes get written, etc. + /// + /// + /// + /// + /// The HTML tag context. + /// The HTML writer. + public delegate void HtmlTagCallback (HtmlTagContext tagContext, HtmlWriter htmlWriter); +} diff --git a/src/MimeKit/Text/HtmlTagContext.cs b/src/MimeKit/Text/HtmlTagContext.cs new file mode 100644 index 0000000..ac36b76 --- /dev/null +++ b/src/MimeKit/Text/HtmlTagContext.cs @@ -0,0 +1,223 @@ +// +// HtmlTagContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; + +namespace MimeKit.Text { + /// + /// An HTML tag context. + /// + /// + /// An HTML tag context used with the delegate. + /// + /// + /// + /// + public abstract class HtmlTagContext + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The HTML tag identifier. + /// + /// is invalid. + /// + protected HtmlTagContext (HtmlTagId tagId) + { + TagId = tagId; + } + + /// + /// Get the HTML tag attributes. + /// + /// + /// Gets the HTML tag attributes. + /// + /// + /// + /// + /// The attributes. + public abstract HtmlAttributeCollection Attributes { + get; + } + + /// + /// Get or set whether or not the end tag should be deleted. + /// + /// + /// Gets or sets whether or not the end tag should be deleted. + /// + /// true if the end tag should be deleted; otherwise, false. + public bool DeleteEndTag { + get; set; + } + + /// + /// Get or set whether or not the tag should be deleted. + /// + /// + /// Gets or sets whether or not the tag should be deleted. + /// + /// true if the tag should be deleted; otherwise, false. + public bool DeleteTag { + get; set; + } + + /// + /// Get or set whether or not the should be invoked for the end tag. + /// + /// + /// Gets or sets whether or not the should be invoked for the end tag. + /// + /// + /// + /// + /// true if the callback should be invoked for end tag; otherwise, false. + public bool InvokeCallbackForEndTag { + get; set; + } + + /// + /// Get whether or not the tag is an empty element. + /// + /// + /// Gets whether or not the tag is an empty element. + /// + /// + /// + /// + /// true if the tag is an empty element; otherwise, false. + public abstract bool IsEmptyElementTag { + get; + } + + /// + /// Get whether or not the tag is an end tag. + /// + /// + /// Gets whether or not the tag is an end tag. + /// + /// + /// + /// + /// true if the tag is an end tag; otherwise, false. + public abstract bool IsEndTag { + get; + } + + /// + /// Get or set whether or not the inner content of the tag should be suppressed. + /// + /// + /// Gets or sets whether or not the inner content of the tag should be suppressed. + /// + /// true if the inner content should be suppressed; otherwise, false. + public bool SuppressInnerContent { + get; set; + } + + /// + /// Get the HTML tag identifier. + /// + /// + /// Gets the HTML tag identifier. + /// + /// + /// + /// + /// The HTML tag identifier. + public HtmlTagId TagId { + get; private set; + } + + /// + /// Get the HTML tag name. + /// + /// + /// Gets the HTML tag name. + /// + /// + /// + /// + /// The HTML tag name. + public abstract string TagName { + get; + } + + /// + /// Write the HTML tag. + /// + /// + /// Writes the HTML tag to the given . + /// + /// The HTML writer. + /// + /// is null. + /// + public void WriteTag (HtmlWriter htmlWriter) + { + WriteTag (htmlWriter, false); + } + + /// + /// Write the HTML tag. + /// + /// + /// Writes the HTML tag to the given . + /// + /// + /// + /// + /// The HTML writer. + /// true if the should also be written; otherwise, false. + /// + /// is null. + /// + public void WriteTag (HtmlWriter htmlWriter, bool writeAttributes) + { + if (htmlWriter == null) + throw new ArgumentNullException (nameof (htmlWriter)); + + if (IsEndTag) { + htmlWriter.WriteEndTag (TagName); + return; + } + + if (IsEmptyElementTag) + htmlWriter.WriteEmptyElementTag (TagName); + else + htmlWriter.WriteStartTag (TagName); + + if (writeAttributes) { + for (int i = 0; i < Attributes.Count; i++) + htmlWriter.WriteAttribute (Attributes[i]); + } + } + } +} diff --git a/src/MimeKit/Text/HtmlTagId.cs b/src/MimeKit/Text/HtmlTagId.cs new file mode 100644 index 0000000..a6f57ce --- /dev/null +++ b/src/MimeKit/Text/HtmlTagId.cs @@ -0,0 +1,871 @@ +// +// HtmlTagId.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; + +using MimeKit.Utils; + +namespace MimeKit.Text { + /// + /// HTML tag identifiers. + /// + /// + /// HTML tag identifiers. + /// + /// + /// + /// + public enum HtmlTagId { + /// + /// An unknown HTML tag identifier. + /// + Unknown, + + /// + /// The HTML <a> tag. + /// + A, + + /// + /// The HTML <abbr> tag. + /// + Abbr, + + /// + /// The HTML <acronym> tag. + /// + Acronym, + + /// + /// The HTML <address> tag. + /// + Address, + + /// + /// The HTML <applet> tag. + /// + Applet, + + /// + /// The HTML <area> tag. + /// + Area, + + /// + /// The HTML <article> tag. + /// + Article, + + /// + /// The HTML <aside> tag. + /// + Aside, + + /// + /// The HTML <audio> tag. + /// + Audio, + + /// + /// The HTML <b> tag. + /// + B, + + /// + /// The HTML <base> tag. + /// + Base, + + /// + /// The HTML <basefont> tag. + /// + BaseFont, + + /// + /// The HTML <bdi> tag. + /// + Bdi, + + /// + /// The HTML <bdo> tag. + /// + Bdo, + + /// + /// The HTML <bgsound> tag. + /// + BGSound, + + /// + /// The HTML <big> tag. + /// + Big, + + /// + /// The HTML <blink> tag. + /// + Blink, + + /// + /// The HTML <blockquote> tag. + /// + BlockQuote, + + /// + /// The HTML <body> tag. + /// + Body, + + /// + /// The HTML <br> tag. + /// + Br, + + /// + /// The HTML <button> tag. + /// + Button, + + /// + /// The HTML <canvas> tag. + /// + Canvas, + + /// + /// The HTML <caption> tag. + /// + Caption, + + /// + /// The HTML <center> tag. + /// + Center, + + /// + /// The HTML <cite> tag. + /// + Cite, + + /// + /// The HTML <code> tag. + /// + Code, + + /// + /// The HTML <col> tag. + /// + Col, + + /// + /// The HTML <colgroup> tag. + /// + ColGroup, + + /// + /// The HTML <command> tag. + /// + Command, + + /// + /// The HTML comment tag. + /// + Comment, + + /// + /// The HTML <datalist> tag. + /// + DataList, + + /// + /// The HTML <dd> tag. + /// + DD, + + /// + /// The HTML <del> tag. + /// + Del, + + /// + /// The HTML <details> tag. + /// + Details, + + /// + /// The HTML <dfn> tag. + /// + Dfn, + + /// + /// The HTML <dialog> tag. + /// + Dialog, + + /// + /// The HTML <dir> tag. + /// + Dir, + + /// + /// The HTML <div> tag. + /// + Div, + + /// + /// The HTML <dl> tag. + /// + DL, + + /// + /// The HTML <dt> tag. + /// + DT, + + /// + /// The HTML <em> tag. + /// + EM, + + /// + /// The HTML <embed> tag. + /// + Embed, + + /// + /// The HTML <fieldset> tag. + /// + FieldSet, + + /// + /// The HTML <figcaption> tag. + /// + FigCaption, + + /// + /// The HTML <figure> tag. + /// + Figure, + + /// + /// The HTML <font> tag. + /// + Font, + + /// + /// The HTML <footer> tag. + /// + Footer, + + /// + /// The HTML <form> tag. + /// + Form, + + /// + /// The HTML <frame> tag. + /// + Frame, + + /// + /// The HTML <frameset> tag. + /// + FrameSet, + + /// + /// The HTML <h1> tag. + /// + H1, + + /// + /// The HTML <h2> tag. + /// + H2, + + /// + /// The HTML <h3> tag. + /// + H3, + + /// + /// The HTML <h4> tag. + /// + H4, + + /// + /// The HTML <h5> tag. + /// + H5, + + /// + /// The HTML <h6> tag. + /// + H6, + + /// + /// The HTML <head> tag. + /// + Head, + + /// + /// The HTML <header> tag. + /// + Header, + + /// + /// The HTML <hr> tag. + /// + HR, + + /// + /// The HTML <html> tag. + /// + Html, + + /// + /// The HTML <i> tag. + /// + I, + + /// + /// The HTML <iframe> tag. + /// + IFrame, + + /// + /// The HTML <image> tag. + /// + [HtmlTagName ("img")] + Image, + + /// + /// The HTML <input> tag. + /// + Input, + + /// + /// The HTML <ins> tag. + /// + Ins, + + /// + /// The HTML <isindex> tag. + /// + IsIndex, + + /// + /// The HTML <kbd> tag. + /// + Kbd, + + /// + /// The HTML <keygen> tag. + /// + Keygen, + + /// + /// The HTML <label> tag. + /// + Label, + + /// + /// The HTML <legend> tag. + /// + Legend, + + /// + /// The HTML <li> tag. + /// + LI, + + /// + /// The HTML <link> tag. + /// + Link, + + /// + /// The HTML <listing> tag. + /// + Listing, + + /// + /// The HTML <main> tag. + /// + Main, + + /// + /// The HTML <map> tag. + /// + Map, + + /// + /// The HTML <mark> tag. + /// + Mark, + + /// + /// The HTML <marquee> tag. + /// + Marquee, + + /// + /// The HTML <menu> tag. + /// + Menu, + + /// + /// The HTML <menuitem> tag. + /// + MenuItem, + + /// + /// The HTML <meta> tag. + /// + Meta, + + /// + /// The HTML <meter> tag. + /// + Meter, + + /// + /// The HTML <nav> tag. + /// + Nav, + + /// + /// The HTML <nextid> tag. + /// + NextId, + + /// + /// The HTML <nobr> tag. + /// + NoBR, + + /// + /// The HTML <noembed> tag. + /// + NoEmbed, + + /// + /// The HTML <noframes> tag. + /// + NoFrames, + + /// + /// The HTML <noscript> tag. + /// + NoScript, + + /// + /// The HTML <object> tag. + /// + Object, + + /// + /// The HTML <ol> tag. + /// + OL, + + /// + /// The HTML <optgroup> tag. + /// + OptGroup, + + /// + /// The HTML <option> tag. + /// + Option, + + /// + /// The HTML <output> tag. + /// + Output, + + /// + /// The HTML <p> tag. + /// + P, + + /// + /// The HTML <param> tag. + /// + Param, + + /// + /// The HTML <plaintext> tag. + /// + PlainText, + + /// + /// The HTML <pre> tag. + /// + Pre, + + /// + /// The HTML <progress> tag. + /// + Progress, + + /// + /// The HTML <q> tag. + /// + Q, + + /// + /// The HTML <rp> tag. + /// + RP, + + /// + /// The HTML <rt> tag. + /// + RT, + + /// + /// The HTML <ruby> tag. + /// + Ruby, + + /// + /// The HTML <s> tag. + /// + S, + + /// + /// The HTML <samp> tag. + /// + Samp, + + /// + /// The HTML <script> tag. + /// + Script, + + /// + /// The HTML <section> tag. + /// + Section, + + /// + /// The HTML <select> tag. + /// + Select, + + /// + /// The HTML <small> tag. + /// + Small, + + /// + /// The HTML <source> tag. + /// + Source, + + /// + /// The HTML <span> tag. + /// + Span, + + /// + /// The HTML <strike> tag. + /// + Strike, + + /// + /// The HTML <strong> tag. + /// + Strong, + + /// + /// The HTML <style> tag. + /// + Style, + + /// + /// The HTML <sub> tag. + /// + Sub, + + /// + /// The HTML <summary> tag. + /// + Summary, + + /// + /// The HTML <sup> tag. + /// + Sup, + + /// + /// The HTML <table> tag. + /// + Table, + + /// + /// The HTML <tbody> tag. + /// + TBody, + + /// + /// The HTML <td> tag. + /// + TD, + + /// + /// The HTML <textarea> tag. + /// + TextArea, + + /// + /// The HTML <tfoot> tag. + /// + Tfoot, + + /// + /// The HTML <th> tag. + /// + TH, + + /// + /// The HTML <thead> tag. + /// + THead, + + /// + /// The HTML <time> tag. + /// + Time, + + /// + /// The HTML <title> tag. + /// + Title, + + /// + /// The HTML <tr> tag. + /// + TR, + + /// + /// The HTML <track> tag. + /// + Track, + + /// + /// The HTML <tt> tag. + /// + TT, + + /// + /// The HTML <u> tag. + /// + U, + + /// + /// The HTML <ul> tag. + /// + UL, + + /// + /// The HTML <var> tag. + /// + Var, + + /// + /// The HTML <video> tag. + /// + Video, + + /// + /// The HTML <wbr> tag. + /// + Wbr, + + /// + /// The HTML <xml> tag. + /// + Xml, + + /// + /// The HTML <xmp> tag. + /// + Xmp, + } + + [AttributeUsage (AttributeTargets.Field)] + class HtmlTagNameAttribute : Attribute { + public HtmlTagNameAttribute (string name) + { + Name = name; + } + + public string Name { + get; protected set; + } + } + + /// + /// extension methods. + /// + /// + /// extension methods. + /// + public static class HtmlTagIdExtensions + { + static readonly Dictionary TagNameToId; + + static HtmlTagIdExtensions () + { + var values = (HtmlTagId[]) Enum.GetValues (typeof (HtmlTagId)); + + TagNameToId = new Dictionary (values.Length - 1, MimeUtils.OrdinalIgnoreCase); + + for (int i = 0; i < values.Length - 1; i++) + TagNameToId.Add (values[i].ToHtmlTagName (), values[i]); + } + + /// + /// Converts the enum value into the equivalent tag name. + /// + /// + /// Converts the enum value into the equivalent tag name. + /// + /// The tag name. + /// The enum value. + public static string ToHtmlTagName (this HtmlTagId value) + { + if (value == HtmlTagId.Comment) + return "!"; + + var name = value.ToString (); + +#if NETSTANDARD1_3 || NETSTANDARD1_6 + var field = typeof (HtmlTagId).GetTypeInfo ().GetDeclaredField (name); + var attrs = field.GetCustomAttributes (typeof (HtmlTagNameAttribute), false).ToArray (); +#else + var field = typeof (HtmlTagId).GetField (name); + var attrs = field.GetCustomAttributes (typeof (HtmlTagNameAttribute), false); +#endif + + if (attrs != null && attrs.Length == 1) + return ((HtmlTagNameAttribute) attrs[0]).Name; + + return name.ToLowerInvariant (); + } + + /// + /// Converts the tag name into the equivalent tag id. + /// + /// + /// Converts the tag name into the equivalent tag id. + /// + /// The tag id. + /// The tag name. + internal static HtmlTagId ToHtmlTagId (this string name) + { + HtmlTagId value; + + if (string.IsNullOrEmpty (name)) + return HtmlTagId.Unknown; + + if (name[0] == '!') + return HtmlTagId.Comment; + + if (!TagNameToId.TryGetValue (name, out value)) + return HtmlTagId.Unknown; + + return value; + } + + /// + /// Determines whether or not the HTML tag is an empty element. + /// + /// + /// Determines whether or not the HTML tag is an empty element. + /// + /// true if the tag is an empty element; otherwise, false. + /// Identifier. + public static bool IsEmptyElement (this HtmlTagId id) + { + switch (id) { + case HtmlTagId.Area: + case HtmlTagId.Base: + case HtmlTagId.Br: + case HtmlTagId.Col: + case HtmlTagId.Command: + case HtmlTagId.Embed: + case HtmlTagId.HR: + case HtmlTagId.Image: + case HtmlTagId.Input: + case HtmlTagId.Keygen: + case HtmlTagId.Link: + case HtmlTagId.Meta: + case HtmlTagId.Param: + case HtmlTagId.Source: + case HtmlTagId.Track: + case HtmlTagId.Wbr: + return true; + default: + return false; + } + } + + /// + /// Determines whether or not the HTML tag is a formatting element. + /// + /// + /// Determines whether or not the HTML tag is a formatting element. + /// + /// true if the HTML tag is a formatting element; otherwise, false. + /// The HTML tag identifier. + public static bool IsFormattingElement (this HtmlTagId id) + { + switch (id) { + case HtmlTagId.A: + case HtmlTagId.B: + case HtmlTagId.Big: + case HtmlTagId.Code: + case HtmlTagId.EM: + case HtmlTagId.Font: + case HtmlTagId.I: + case HtmlTagId.NoBR: + case HtmlTagId.S: + case HtmlTagId.Small: + case HtmlTagId.Strike: + case HtmlTagId.Strong: + case HtmlTagId.TT: + case HtmlTagId.U: + return true; + default: + return false; + } + } + } +} diff --git a/src/MimeKit/Text/HtmlTextPreviewer.cs b/src/MimeKit/Text/HtmlTextPreviewer.cs new file mode 100644 index 0000000..b3e2e11 --- /dev/null +++ b/src/MimeKit/Text/HtmlTextPreviewer.cs @@ -0,0 +1,253 @@ +// +// HtmlTextPreviewer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +namespace MimeKit.Text { + /// + /// A text previewer for HTML content. + /// + /// + /// A text previewer for HTML content. + /// + public class HtmlTextPreviewer : TextPreviewer + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new previewer for HTML. + /// + public HtmlTextPreviewer () + { + } + + /// + /// Get the input format. + /// + /// + /// Gets the input format. + /// + /// The input format. + public override TextFormat InputFormat { + get { return TextFormat.Html; } + } + + static bool IsWhiteSpace (char c) + { + return char.IsWhiteSpace (c) || (c >= 0x200B && c <= 0x200D); + } + + static bool Append (char[] preview, ref int previewLength, string value, ref bool lwsp) + { + int i; + + for (i = 0; i < value.Length && previewLength < preview.Length; i++) { + if (IsWhiteSpace (value[i])) { + if (!lwsp) { + preview[previewLength++] = ' '; + lwsp = true; + } + } else { + preview[previewLength++] = value[i]; + lwsp = false; + } + } + + if (i < value.Length) { + if (lwsp) + previewLength--; + + preview[previewLength - 1] = '\u2026'; + lwsp = false; + return true; + } + + return false; + } + + sealed class HtmlTagContext + { + public HtmlTagContext (HtmlTagId id) + { + TagId = id; + } + + public HtmlTagId TagId { + get; + } + + public int ListIndex { + get; set; + } + + public bool SuppressInnerContent { + get; set; + } + } + + static void Pop (IList stack, HtmlTagId id) + { + for (int i = stack.Count; i > 0; i--) { + if (stack[i - 1].TagId == id) { + stack.RemoveAt (i - 1); + break; + } + } + } + + static bool ShouldSuppressInnerContent (HtmlTagId id) + { + switch (id) { + case HtmlTagId.OL: + case HtmlTagId.Script: + case HtmlTagId.Style: + case HtmlTagId.Table: + case HtmlTagId.TBody: + case HtmlTagId.THead: + case HtmlTagId.TR: + case HtmlTagId.UL: + return true; + default: + return false; + } + } + + static bool SuppressContent (IList stack) + { + int lastIndex = stack.Count - 1; + + return lastIndex >= 0 && stack[lastIndex].SuppressInnerContent; + } + + HtmlTagContext GetListItemContext (IList stack) + { + for (int i = stack.Count; i > 0; i--) { + var ctx = stack[i - 1]; + + if (ctx.TagId == HtmlTagId.OL || ctx.TagId == HtmlTagId.UL) + return ctx; + } + + return null; + } + + /// + /// Get a text preview of a stream of text. + /// + /// + /// Gets a text preview of a stream of text. + /// + /// The original text stream. + /// A string representing a shortened preview of the original text. + /// + /// is null. + /// + public override string GetPreviewText (TextReader reader) + { + if (reader == null) + throw new ArgumentNullException (nameof (reader)); + + var tokenizer = new HtmlTokenizer (reader) { IgnoreTruncatedTags = true }; + var preview = new char[MaximumPreviewLength]; + var stack = new List (); + var prefix = string.Empty; + int previewLength = 0; + HtmlTagContext ctx; + HtmlAttribute attr; + bool body = false; + bool full = false; + bool lwsp = true; + HtmlToken token; + + while (!full && tokenizer.ReadNextToken (out token)) { + switch (token.Kind) { + case HtmlTokenKind.Tag: + var tag = (HtmlTagToken) token; + + if (!tag.IsEndTag) { + if (body) { + switch (tag.Id) { + case HtmlTagId.Image: + if ((attr = tag.Attributes.FirstOrDefault (x => x.Id == HtmlAttributeId.Alt)) != null) { + full = Append (preview, ref previewLength, prefix + attr.Value, ref lwsp); + prefix = string.Empty; + } + break; + case HtmlTagId.LI: + if ((ctx = GetListItemContext (stack)) != null) { + if (ctx.TagId == HtmlTagId.OL) { + full = Append (preview, ref previewLength, $" {++ctx.ListIndex}. ", ref lwsp); + prefix = string.Empty; + } else { + //full = Append (preview, ref previewLength, " \u2022 ", ref lwsp); + prefix = " "; + } + } + break; + case HtmlTagId.Br: + case HtmlTagId.P: + prefix = " "; + break; + } + + if (!tag.IsEmptyElement) { + ctx = new HtmlTagContext (tag.Id) { + SuppressInnerContent = ShouldSuppressInnerContent (tag.Id) + }; + stack.Add (ctx); + } + } else if (tag.Id == HtmlTagId.Body && !tag.IsEmptyElement) { + body = true; + } + } else if (tag.Id == HtmlTagId.Body) { + stack.Clear (); + body = false; + } else { + Pop (stack, tag.Id); + } + break; + case HtmlTokenKind.Data: + if (body && !SuppressContent (stack)) { + var data = (HtmlDataToken) token; + + full = Append (preview, ref previewLength, prefix + data.Data, ref lwsp); + prefix = string.Empty; + } + break; + } + } + + if (lwsp && previewLength > 0) + previewLength--; + + return new string (preview, 0, previewLength); + } + } +} diff --git a/src/MimeKit/Text/HtmlToHtml.cs b/src/MimeKit/Text/HtmlToHtml.cs new file mode 100644 index 0000000..c9b4abf --- /dev/null +++ b/src/MimeKit/Text/HtmlToHtml.cs @@ -0,0 +1,357 @@ +// +// HtmlToHtml.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +namespace MimeKit.Text { + /// + /// An HTML to HTML converter. + /// + /// + /// Used to convert HTML into HTML. + /// + /// + /// + /// + public class HtmlToHtml : TextConverter + { + //static readonly HashSet AutoClosingTags; + + //static HtmlToHtml () + //{ + // // Note: These are tags that auto-close when an identical tag is encountered and/or when a parent node is closed. + // AutoClosingTags = new HashSet (new [] { + // "li", + // "p", + // "td", + // "tr" + // }, MimeUtils.OrdinalIgnoreCase); + //} + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new HTML to HTML converter. + /// + public HtmlToHtml () + { + } + + /// + /// Get the input format. + /// + /// + /// Gets the input format. + /// + /// The input format. + public override TextFormat InputFormat { + get { return TextFormat.Html; } + } + + /// + /// Get the output format. + /// + /// + /// Gets the output format. + /// + /// The output format. + public override TextFormat OutputFormat { + get { return TextFormat.Html; } + } + + /// + /// Get or set whether or not the converter should remove HTML comments from the output. + /// + /// + /// Gets or sets whether or not the converter should remove HTML comments from the output. + /// + /// true if the converter should remove comments; otherwise, false. + public bool FilterComments { + get; set; + } + + /// + /// Get or set whether or not executable scripts should be stripped from the output. + /// + /// + /// Gets or sets whether or not executable scripts should be stripped from the output. + /// + /// true if executable scripts should be filtered; otherwise, false. + public bool FilterHtml { + get; set; + } + + /// + /// Get or set the footer format. + /// + /// + /// Gets or sets the footer format. + /// + /// The footer format. + public HeaderFooterFormat FooterFormat { + get; set; + } + + /// + /// Get or set the header format. + /// + /// + /// Gets or sets the header format. + /// + /// The header format. + public HeaderFooterFormat HeaderFormat { + get; set; + } + + /// + /// Get or set the method to use for custom + /// filtering of HTML tags and content. + /// + /// + /// Get or set the method to use for custom + /// filtering of HTML tags and content. + /// + /// + /// + /// + /// The html tag callback. + public HtmlTagCallback HtmlTagCallback { + get; set; + } + +#if false + /// + /// Get or set whether or not the converter should collapse white space, + /// balance tags, and fix other problems in the source HTML. + /// + /// + /// Gets or sets whether or not the converter should collapse white space, + /// balance tags, and fix other problems in the source HTML. + /// + /// true if the output html should be normalized; otherwise, false. + public bool NormalizeHtml { + get; set; + } +#endif + +#if false + /// + /// Get or set whether or not the converter should only output an HTML fragment. + /// + /// + /// Gets or sets whether or not the converter should only output an HTML fragment. + /// + /// true if the converter should only output an HTML fragment; otherwise, false. + public bool OutputHtmlFragment { + get; set; + } +#endif + + class HtmlToHtmlTagContext : HtmlTagContext + { + readonly HtmlTagToken tag; + + public HtmlToHtmlTagContext (HtmlTagToken htmlTag) : base (htmlTag.Id) + { + tag = htmlTag; + } + + public override string TagName { + get { return tag.Name; } + } + + public override HtmlAttributeCollection Attributes { + get { return tag.Attributes; } + } + + public override bool IsEmptyElementTag { + get { return tag.IsEmptyElement || tag.Id.IsEmptyElement (); } + } + + public override bool IsEndTag { + get { return tag.IsEndTag; } + } + } + + static void DefaultHtmlTagCallback (HtmlTagContext tagContext, HtmlWriter htmlWriter) + { + tagContext.WriteTag (htmlWriter, true); + } + + static bool SuppressContent (IList stack) + { + for (int i = stack.Count; i > 0; i--) { + if (stack[i - 1].SuppressInnerContent) + return true; + } + + return false; + } + + static HtmlToHtmlTagContext Pop (IList stack, string name) + { + for (int i = stack.Count; i > 0; i--) { + if (stack[i - 1].TagName.Equals (name, StringComparison.OrdinalIgnoreCase)) { + var ctx = stack[i - 1]; + stack.RemoveAt (i - 1); + return ctx; + } + } + + return null; + } + + /// + /// Convert the contents of from the to the + /// and uses the to write the resulting text. + /// + /// + /// Converts the contents of from the to the + /// and uses the to write the resulting text. + /// + /// The text reader. + /// The text writer. + /// + /// is null. + /// -or- + /// is null. + /// + public override void Convert (TextReader reader, TextWriter writer) + { + if (reader == null) + throw new ArgumentNullException (nameof (reader)); + + if (writer == null) + throw new ArgumentNullException (nameof (writer)); + + if (!string.IsNullOrEmpty (Header)) { + if (HeaderFormat == HeaderFooterFormat.Text) { + var converter = new TextToHtml { OutputHtmlFragment = true }; + + using (var sr = new StringReader (Header)) + converter.Convert (sr, writer); + } else { + writer.Write (Header); + } + } + + using (var htmlWriter = new HtmlWriter (writer)) { + var callback = HtmlTagCallback ?? DefaultHtmlTagCallback; + var stack = new List (); + var tokenizer = new HtmlTokenizer (reader); + HtmlToHtmlTagContext ctx; + HtmlToken token; + + while (tokenizer.ReadNextToken (out token)) { + switch (token.Kind) { + default: + if (!SuppressContent (stack)) + htmlWriter.WriteToken (token); + break; + case HtmlTokenKind.Comment: + if (!FilterComments && !SuppressContent (stack)) + htmlWriter.WriteToken (token); + break; + case HtmlTokenKind.Tag: + var tag = (HtmlTagToken) token; + + if (!tag.IsEndTag) { + //if (NormalizeHtml && AutoClosingTags.Contains (startTag.TagName) && + // (ctx = Pop (stack, startTag.TagName)) != null && + // ctx.InvokeCallbackForEndTag && !SuppressContent (stack)) { + // var value = string.Format ("", ctx.TagName); + // var name = ctx.TagName; + // + // ctx = new HtmlToHtmlTagContext (new HtmlTokenTag (HtmlTokenKind.EndTag, name, value)) { + // InvokeCallbackForEndTag = ctx.InvokeCallbackForEndTag, + // SuppressInnerContent = ctx.SuppressInnerContent, + // DeleteEndTag = ctx.DeleteEndTag, + // DeleteTag = ctx.DeleteTag + // }; + // callback (ctx, htmlWriter); + //} + + if (!tag.IsEmptyElement) { + ctx = new HtmlToHtmlTagContext (tag); + + if (FilterHtml && ctx.TagId == HtmlTagId.Script) { + ctx.SuppressInnerContent = true; + ctx.DeleteEndTag = true; + ctx.DeleteTag = true; + } else if (!SuppressContent (stack)) { + callback (ctx, htmlWriter); + } + + stack.Add (ctx); + } else if (!SuppressContent (stack)) { + ctx = new HtmlToHtmlTagContext (tag); + + if (!FilterHtml || ctx.TagId != HtmlTagId.Script) + callback (ctx, htmlWriter); + } + } else { + if ((ctx = Pop (stack, tag.Name)) != null) { + if (!SuppressContent (stack)) { + if (ctx.InvokeCallbackForEndTag) { + ctx = new HtmlToHtmlTagContext (tag) { + InvokeCallbackForEndTag = ctx.InvokeCallbackForEndTag, + SuppressInnerContent = ctx.SuppressInnerContent, + DeleteEndTag = ctx.DeleteEndTag, + DeleteTag = ctx.DeleteTag + }; + callback (ctx, htmlWriter); + } else if (!ctx.DeleteEndTag) { + htmlWriter.WriteEndTag (tag.Name); + } + } + } else if (!SuppressContent (stack)) { + ctx = new HtmlToHtmlTagContext (tag); + callback (ctx, htmlWriter); + } + } + break; + } + } + + htmlWriter.Flush (); + } + + if (!string.IsNullOrEmpty (Footer)) { + if (FooterFormat == HeaderFooterFormat.Text) { + var converter = new TextToHtml { OutputHtmlFragment = true }; + + using (var sr = new StringReader (Footer)) + converter.Convert (sr, writer); + } else { + writer.Write (Footer); + } + } + } + } +} diff --git a/src/MimeKit/Text/HtmlToken.cs b/src/MimeKit/Text/HtmlToken.cs new file mode 100644 index 0000000..fd85943 --- /dev/null +++ b/src/MimeKit/Text/HtmlToken.cs @@ -0,0 +1,656 @@ +// +// HtmlToken.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Collections.Generic; + +namespace MimeKit.Text { + /// + /// An abstract HTML token class. + /// + /// + /// An abstract HTML token class. + /// + public abstract class HtmlToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The kind of token. + protected HtmlToken (HtmlTokenKind kind) + { + Kind = kind; + } + + /// + /// Get the kind of HTML token that this object represents. + /// + /// + /// Gets the kind of HTML token that this object represents. + /// + /// The kind of token. + public HtmlTokenKind Kind { + get; private set; + } + + /// + /// Write the HTML token to a . + /// + /// + /// Writes the HTML token to a . + /// + /// The output. + /// + /// is null. + /// + public abstract void WriteTo (TextWriter output); + + /// + /// Returns a that represents the current . + /// + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + using (var output = new StringWriter ()) { + WriteTo (output); + + return output.ToString (); + } + } + } + + /// + /// An HTML comment token. + /// + /// + /// An HTML comment token. + /// + public class HtmlCommentToken : HtmlToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The comment text. + /// true if the comment is bogus; otherwise, false. + /// + /// is null. + /// + public HtmlCommentToken (string comment, bool bogus = false) : base (HtmlTokenKind.Comment) + { + if (comment == null) + throw new ArgumentNullException (nameof (comment)); + + IsBogusComment = bogus; + Comment = comment; + } + + /// + /// Get the comment. + /// + /// + /// Gets the comment. + /// + /// The comment. + public string Comment { + get; private set; + } + + /// + /// Get whether or not the comment is a bogus comment. + /// + /// + /// Gets whether or not the comment is a bogus comment. + /// + /// true if the comment is bogus; otherwise, false. + public bool IsBogusComment { + get; private set; + } + + internal bool IsBangComment { + get; set; + } + + /// + /// Write the HTML comment to a . + /// + /// + /// Writes the HTML comment to a . + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (!IsBogusComment) { + output.Write (""); + } else { + output.Write ('<'); + if (IsBangComment) + output.Write ('!'); + output.Write (Comment); + output.Write ('>'); + } + } + } + + /// + /// An HTML token constisting of character data. + /// + /// + /// An HTML token consisting of character data. + /// + public class HtmlDataToken : HtmlToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The kind of character data. + /// The character data. + /// + /// is not a valid . + /// + /// + /// is null. + /// + protected HtmlDataToken (HtmlTokenKind kind, string data) : base (kind) + { + switch (kind) { + default: throw new ArgumentOutOfRangeException (nameof (kind)); + case HtmlTokenKind.ScriptData: + case HtmlTokenKind.CData: + case HtmlTokenKind.Data: + break; + } + + if (data == null) + throw new ArgumentNullException (nameof (data)); + + Data = data; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The character data. + /// + /// is null. + /// + public HtmlDataToken (string data) : base (HtmlTokenKind.Data) + { + if (data == null) + throw new ArgumentNullException (nameof (data)); + + Data = data; + } + + internal bool EncodeEntities { + get; set; + } + + /// + /// Get the character data. + /// + /// + /// Gets the character data. + /// + /// The character data. + public string Data { + get; private set; + } + + /// + /// Write the HTML character data to a . + /// + /// + /// Writes the HTML character data to a , + /// encoding it if it isn't already encoded. + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + if (!EncodeEntities) { + output.Write (Data); + return; + } + + HtmlUtils.HtmlEncode (output, Data); + } + } + + /// + /// An HTML token constisting of [CDATA[. + /// + /// + /// An HTML token consisting of [CDATA[. + /// + public class HtmlCDataToken : HtmlDataToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The character data. + /// + /// is null. + /// + public HtmlCDataToken (string data) : base (HtmlTokenKind.CData, data) + { + } + + /// + /// Write the HTML character data to a . + /// + /// + /// Writes the HTML character data to a , + /// encoding it if it isn't already encoded. + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + output.Write (""); + } + } + + /// + /// An HTML token constisting of script data. + /// + /// + /// An HTML token consisting of script data. + /// + public class HtmlScriptDataToken : HtmlDataToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The script data. + /// + /// is null. + /// + public HtmlScriptDataToken (string data) : base (HtmlTokenKind.ScriptData, data) + { + } + + /// + /// Write the HTML script data to a . + /// + /// + /// Writes the HTML script data to a , + /// encoding it if it isn't already encoded. + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + output.Write (Data); + } + } + + /// + /// An HTML tag token. + /// + /// + /// An HTML tag token. + /// + public class HtmlTagToken : HtmlToken + { + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The name of the tag. + /// The attributes. + /// true if the tag is an empty element; otherwise, false. + /// + /// is null. + /// -or- + /// is null. + /// + public HtmlTagToken (string name, IEnumerable attributes, bool isEmptyElement) : base (HtmlTokenKind.Tag) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + if (attributes == null) + throw new ArgumentNullException (nameof (attributes)); + + Attributes = new HtmlAttributeCollection (attributes); + IsEmptyElement = isEmptyElement; + Id = name.ToHtmlTagId (); + Name = name; + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The name of the tag. + /// true if the tag is an end tag; otherwise, false. + /// + /// is null. + /// + public HtmlTagToken (string name, bool isEndTag) : base (HtmlTokenKind.Tag) + { + if (name == null) + throw new ArgumentNullException (nameof (name)); + + Attributes = new HtmlAttributeCollection (); + Id = name.ToHtmlTagId (); + IsEndTag = isEndTag; + Name = name; + } + + /// + /// Get the attributes. + /// + /// + /// Gets the attributes. + /// + /// The attributes. + public HtmlAttributeCollection Attributes { + get; private set; + } + + /// + /// Get the HTML tag identifier. + /// + /// + /// Gets the HTML tag identifier. + /// + /// The HTML tag identifier. + public HtmlTagId Id { + get; private set; + } + + /// + /// Get whether or not the tag is an empty element. + /// + /// + /// Gets whether or not the tag is an empty element. + /// + /// true if the tag is an empty element; otherwise, false. + public bool IsEmptyElement { + get; internal set; + } + + /// + /// Get whether or not the tag is an end tag. + /// + /// + /// Gets whether or not the tag is an end tag. + /// + /// true if the tag is an end tag; otherwise, false. + public bool IsEndTag { + get; private set; + } + + /// + /// Get the name of the tag. + /// + /// + /// Gets the name of the tag. + /// + /// The name. + public string Name { + get; private set; + } + + /// + /// Write the HTML tag to a . + /// + /// + /// Writes the HTML tag to a . + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + output.Write ('<'); + if (IsEndTag) + output.Write ('/'); + output.Write (Name); + for (int i = 0; i < Attributes.Count; i++) { + output.Write (' '); + output.Write (Attributes[i].Name); + if (Attributes[i].Value != null) { + output.Write ('='); + HtmlUtils.HtmlAttributeEncode (output, Attributes[i].Value); + } + } + if (IsEmptyElement) + output.Write ('/'); + output.Write ('>'); + } + } + + /// + /// An HTML DOCTYPE token. + /// + /// + /// An HTML DOCTYPE token. + /// + public class HtmlDocTypeToken : HtmlToken + { + string publicIdentifier; + string systemIdentifier; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + public HtmlDocTypeToken () : base (HtmlTokenKind.DocType) + { + RawTagName = "DOCTYPE"; + } + + internal string RawTagName { + get; set; + } + + /// + /// Get whether or not quirks-mode should be forced. + /// + /// + /// Gets whether or not quirks-mode should be forced. + /// + /// true if quirks-mode should be forced; otherwise, false. + public bool ForceQuirksMode { + get; set; + } + + /// + /// Get or set the DOCTYPE name. + /// + /// + /// Gets or sets the DOCTYPE name. + /// + /// The name. + public string Name { + get; set; + } + + /// + /// Get or set the public identifier. + /// + /// + /// Gets or sets the public identifier. + /// + /// The public identifier. + public string PublicIdentifier { + get { return publicIdentifier; } + set { + publicIdentifier = value; + if (value != null) { + if (PublicKeyword == null) + PublicKeyword = "PUBLIC"; + } else { + if (systemIdentifier != null) + SystemKeyword = "SYSTEM"; + } + } + } + + /// + /// Get the public keyword that was used. + /// + /// + /// Gets the public keyword that was used. + /// + /// The public keyword or null if it wasn't used. + public string PublicKeyword { + get; internal set; + } + + /// + /// Get or set the system identifier. + /// + /// + /// Gets or sets the system identifier. + /// + /// The system identifier. + public string SystemIdentifier { + get { return systemIdentifier; } + set { + systemIdentifier = value; + if (value != null) { + if (publicIdentifier == null && SystemKeyword == null) + SystemKeyword = "SYSTEM"; + } else { + SystemKeyword = null; + } + } + } + + /// + /// Get the system keyword that was used. + /// + /// + /// Gets the system keyword that was used. + /// + /// The system keyword or null if it wasn't used. + public string SystemKeyword { + get; internal set; + } + + /// + /// Write the DOCTYPE tag to a . + /// + /// + /// Writes the DOCTYPE tag to a . + /// + /// The output. + /// + /// is null. + /// + public override void WriteTo (TextWriter output) + { + if (output == null) + throw new ArgumentNullException (nameof (output)); + + output.Write ("'); + } + } +} diff --git a/src/MimeKit/Text/HtmlTokenKind.cs b/src/MimeKit/Text/HtmlTokenKind.cs new file mode 100644 index 0000000..0ff55fa --- /dev/null +++ b/src/MimeKit/Text/HtmlTokenKind.cs @@ -0,0 +1,65 @@ +// +// HtmlTokenKind.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +namespace MimeKit.Text { + /// + /// The kinds of tokens that the can emit. + /// + /// + /// The kinds of tokens that the can emit. + /// + public enum HtmlTokenKind { + /// + /// A token consisting of [CDATA[. + /// + CData, + + /// + /// An HTML comment token. + /// + Comment, + + /// + /// A token consisting of character data. + /// + Data, + + /// + /// An HTML DOCTYPE token. + /// + DocType, + + /// + /// A token consisting of script data. + /// + ScriptData, + + /// + /// An HTML tag token. + /// + Tag, + } +} diff --git a/src/MimeKit/Text/HtmlTokenizer.cs b/src/MimeKit/Text/HtmlTokenizer.cs new file mode 100644 index 0000000..b414052 --- /dev/null +++ b/src/MimeKit/Text/HtmlTokenizer.cs @@ -0,0 +1,2937 @@ +// +// HtmlTokenizer.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 .NET Foundation and Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System.IO; +using System.Runtime.CompilerServices; + +namespace MimeKit.Text { + /// + /// An HTML tokenizer. + /// + /// + /// Tokenizes HTML text, emitting an for each token it encounters. + /// + public class HtmlTokenizer + { + // Specification: https://dev.w3.org/html5/spec-LC/tokenization.html + const string DocType = "doctype"; + const string CData = "[CDATA["; + + readonly HtmlEntityDecoder entity = new HtmlEntityDecoder (); + readonly CharBuffer data = new CharBuffer (2048); + readonly CharBuffer name = new CharBuffer (32); + readonly char[] cdata = new char[3]; + readonly TextReader text; + HtmlDocTypeToken doctype; + HtmlAttribute attribute; + string activeTagName; + HtmlTagToken tag; + int cdataIndex; + bool isEndTag; + bool bang; + char quote; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Creates a new . + /// + /// The . + public HtmlTokenizer (TextReader reader) + { + DecodeCharacterReferences = true; + LinePosition = 1; + LineNumber = 1; + text = reader; + } + + /// + /// Get or set whether or not the tokenizer should decode character references. + /// + /// + /// Gets or sets whether or not the tokenizer should decode character references. + /// Character references in attribute values will still be decoded + /// even if this value is set to false. + /// + /// true if character references should be decoded; otherwise, false. + public bool DecodeCharacterReferences { + get; set; + } + + /// + /// Get the current HTML namespace detected by the tokenizer. + /// + /// + /// Gets the current HTML namespace detected by the tokenizer. + /// + /// The html namespace. + public HtmlNamespace HtmlNamespace { + get; private set; + } + + /// + /// Get or set whether or not the tokenizer should ignore truncated tags. + /// + /// + /// Gets or sets whether or not the tokenizer should ignore truncated tags. + /// If false and the stream abrubtly ends in the middle of an HTML tag, it will be + /// treated as an instead. + /// + /// true if truncated tags should be ignored; otherwise, false. + public bool IgnoreTruncatedTags { + get; set; + } + + /// + /// Gets the current line number. + /// + /// + /// This property is most commonly used for error reporting, but can be called + /// at any time. The starting value for this property is 1. + /// Combined with , a value of 1,1 indicates + /// the start of the document. + /// + /// The current line number. + public int LineNumber { + get; private set; + } + + /// + /// Gets the current line position. + /// + /// + /// This property is most commonly used for error reporting, but can be called + /// at any time. The starting value for this property is 1. + /// Combined with , a value of 1,1 indicates + /// the start of the document. + /// + /// The column position of the current line. + public int LinePosition { + get; private set; + } + + /// + /// Get the current state of the tokenizer. + /// + /// + /// Gets the current state of the tokenizer. + /// + /// The current state of the tokenizer. + public HtmlTokenizerState TokenizerState { + get; private set; + } + + /// + /// Create a DOCTYPE token. + /// + /// + /// Creates a DOCTYPE token. + /// + /// The DOCTYPE token. + protected virtual HtmlDocTypeToken CreateDocType () + { + return new HtmlDocTypeToken (); + } + + HtmlDocTypeToken CreateDocTypeToken (string rawTagName) + { + var token = CreateDocType (); + token.RawTagName = rawTagName; + return token; + } + + /// + /// Create an HTML comment token. + /// + /// + /// Creates an HTML comment token. + /// + /// The HTML comment token. + /// The comment. + /// true if the comment is bogus; otherwise, false. + protected virtual HtmlCommentToken CreateCommentToken (string comment, bool bogus = false) + { + return new HtmlCommentToken (comment, bogus); + } + + /// + /// Create an HTML character data token. + /// + /// + /// Creates an HTML character data token. + /// + /// The HTML character data token. + /// The character data. + protected virtual HtmlDataToken CreateDataToken (string data) + { + return new HtmlDataToken (data); + } + + /// + /// Create an HTML character data token. + /// + /// + /// Creates an HTML character data token. + /// + /// The HTML character data token. + /// The character data. + protected virtual HtmlCDataToken CreateCDataToken (string data) + { + return new HtmlCDataToken (data); + } + + /// + /// Create an HTML script data token. + /// + /// + /// Creates an HTML script data token. + /// + /// The HTML script data token. + /// The script data. + protected virtual HtmlScriptDataToken CreateScriptDataToken (string data) + { + return new HtmlScriptDataToken (data); + } + + /// + /// Create an HTML tag token. + /// + /// + /// Creates an HTML tag token. + /// + /// The HTML tag token. + /// The tag name. + /// true if the tag is an end tag; otherwise, false. + protected virtual HtmlTagToken CreateTagToken (string name, bool isEndTag = false) + { + return new HtmlTagToken (name, isEndTag); + } + + /// + /// Create an attribute. + /// + /// + /// Creates an attribute. + /// + /// The attribute. + /// The attribute name. + protected virtual HtmlAttribute CreateAttribute (string name) + { + return new HtmlAttribute (name); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + static bool IsAlphaNumeric (int c) + { + return ((uint) (c - 'A') <= 'Z' - 'A') || ((uint) (c - 'a') <= 'z' - 'a') || ((uint) (c - '0') <= '9' - '0'); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + static bool IsAsciiLetter (int c) + { + return ((uint) (c - 'A') <= 'Z' - 'A') || ((uint) (c - 'a') <= 'z' - 'a'); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + static char ToLower (int c) + { + // check if the char is within the uppercase range + if ((uint) (c - 'A') <= 'Z' - 'A') + return (char) (c + 0x20); + + return (char) c; + } + + int Peek () + { + return text.Peek (); + } + + int Read () + { + int c; + + if ((c = text.Read ()) == -1) + return -1; + + if (c == '\n') { + LinePosition = 1; + LineNumber++; + } else { + LinePosition++; + } + + return c; + } + + // Note: value must be lowercase + bool NameIs (string value) + { + if (name.Length != value.Length) + return false; + + for (int i = 0; i < name.Length; i++) { + if (ToLower (name[i]) != value[i]) + return false; + } + + return true; + } + + void EmitTagAttribute () + { + attribute = CreateAttribute (name.ToString ()); + tag.Attributes.Add (attribute); + name.Length = 0; + } + + HtmlToken EmitCommentToken (string comment, bool bogus = false) + { + var token = CreateCommentToken (comment, bogus); + token.IsBangComment = bang; + data.Length = 0; + name.Length = 0; + bang = false; + return token; + } + + HtmlToken EmitCommentToken (CharBuffer comment, bool bogus = false) + { + return EmitCommentToken (comment.ToString (), bogus); + } + + HtmlToken EmitDocType () + { + var token = doctype; + data.Length = 0; + doctype = null; + return token; + } + + HtmlToken EmitDataToken (bool encodeEntities, bool truncated) + { + if (data.Length == 0) + return null; + + if (truncated && IgnoreTruncatedTags) { + data.Length = 0; + return null; + } + + var token = CreateDataToken (data.ToString ()); + token.EncodeEntities = encodeEntities; + data.Length = 0; + + return token; + } + + HtmlToken EmitCDataToken () + { + if (data.Length == 0) + return null; + + var token = CreateCDataToken (data.ToString ()); + data.Length = 0; + + return token; + } + + HtmlToken EmitScriptDataToken () + { + if (data.Length == 0) + return null; + + var token = CreateScriptDataToken (data.ToString ()); + data.Length = 0; + + return token; + } + + HtmlToken EmitTagToken () + { + if (!tag.IsEndTag && !tag.IsEmptyElement) { + switch (tag.Id) { + case HtmlTagId.Style: case HtmlTagId.Xmp: case HtmlTagId.IFrame: case HtmlTagId.NoEmbed: case HtmlTagId.NoFrames: + TokenizerState = HtmlTokenizerState.RawText; + activeTagName = tag.Name.ToLowerInvariant (); + break; + case HtmlTagId.Title: case HtmlTagId.TextArea: + TokenizerState = HtmlTokenizerState.RcData; + activeTagName = tag.Name.ToLowerInvariant (); + break; + case HtmlTagId.PlainText: + TokenizerState = HtmlTokenizerState.PlainText; + break; + case HtmlTagId.Script: + TokenizerState = HtmlTokenizerState.ScriptData; + break; + case HtmlTagId.NoScript: + // TODO: only switch into the RawText state if scripting is enabled + TokenizerState = HtmlTokenizerState.RawText; + activeTagName = tag.Name.ToLowerInvariant (); + break; + case HtmlTagId.Html: + TokenizerState = HtmlTokenizerState.Data; + + for (int i = tag.Attributes.Count; i > 0; i--) { + var attr = tag.Attributes[i - 1]; + + if (attr.Id == HtmlAttributeId.XmlNS && attr.Value != null) { + HtmlNamespace = attr.Value.ToHtmlNamespace (); + break; + } + } + break; + default: + TokenizerState = HtmlTokenizerState.Data; + break; + } + } else { + TokenizerState = HtmlTokenizerState.Data; + } + + var token = tag; + data.Length = 0; + tag = null; + + return token; + } + + // 8.2.4.69 Tokenizing character references + HtmlToken ReadCharacterReference (HtmlTokenizerState next) + { + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + data.Append ('&'); + + return EmitDataToken (true, false); + } + + c = (char) nc; + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': case '<': case '&': + // no character is consumed, emit '&' + TokenizerState = next; + data.Append ('&'); + return null; + } + + entity.Push ('&'); + + while (entity.Push (c)) { + Read (); + + if (c == ';') + break; + + if ((nc = Peek ()) == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + data.Append (entity.GetPushedInput ()); + entity.Reset (); + + return EmitDataToken (true, false); + } + + c = (char) nc; + } + + TokenizerState = next; + + data.Append (entity.GetValue ()); + entity.Reset (); + + return null; + } + + HtmlToken ReadGenericRawTextLessThan (HtmlTokenizerState rawText, HtmlTokenizerState rawTextEndTagOpen) + { + int nc = Peek (); + + data.Append ('<'); + + switch ((char) nc) { + case '/': + TokenizerState = rawTextEndTagOpen; + data.Append ('/'); + name.Length = 0; + Read (); + break; + default: + TokenizerState = rawText; + break; + } + + return null; + } + + HtmlToken ReadGenericRawTextEndTagOpen (bool decoded, HtmlTokenizerState rawText, HtmlTokenizerState rawTextEndTagName) + { + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitDataToken (decoded, true); + } + + c = (char) nc; + + if (IsAsciiLetter (c)) { + TokenizerState = rawTextEndTagName; + name.Append (c); + data.Append (c); + Read (); + } else { + TokenizerState = rawText; + } + + return null; + } + + HtmlToken ReadGenericRawTextEndTagName (bool decoded, HtmlTokenizerState rawText) + { + var current = TokenizerState; + + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitDataToken (decoded, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + if (NameIs (activeTagName)) { + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + break; + } + + goto default; + case '/': + if (NameIs (activeTagName)) { + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + break; + } + goto default; + case '>': + if (NameIs (activeTagName)) { + var token = CreateTagToken (name.ToString (), true); + TokenizerState = HtmlTokenizerState.Data; + data.Length = 0; + name.Length = 0; + return token; + } + goto default; + default: + if (!IsAsciiLetter (c)) { + TokenizerState = rawText; + return null; + } + + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == current); + + tag = CreateTagToken (name.ToString (), true); + name.Length = 0; + + return null; + } + + // 8.2.4.1 Data state + HtmlToken ReadData () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + break; + } + + c = (char) nc; + + switch (c) { + case '&': + if (DecodeCharacterReferences) { + TokenizerState = HtmlTokenizerState.CharacterReferenceInData; + return null; + } + + goto default; + case '<': + TokenizerState = HtmlTokenizerState.TagOpen; + break; + //case 0: // parse error, but emit it anyway + default: + data.Append (c); + break; + } + } while (TokenizerState == HtmlTokenizerState.Data); + + return EmitDataToken (DecodeCharacterReferences, false); + } + + // 8.2.4.2 Character reference in data state + HtmlToken ReadCharacterReferenceInData () + { + return ReadCharacterReference (HtmlTokenizerState.Data); + } + + // 8.2.4.3 RCDATA state + HtmlToken ReadRcData () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + break; + } + + c = (char) nc; + + switch (c) { + case '&': + if (DecodeCharacterReferences) { + TokenizerState = HtmlTokenizerState.CharacterReferenceInRcData; + return null; + } + + goto default; + case '<': + TokenizerState = HtmlTokenizerState.RcDataLessThan; + return EmitDataToken (DecodeCharacterReferences, false); + default: + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.RcData); + + return EmitDataToken (DecodeCharacterReferences, false); + } + + // 8.2.4.4 Character reference in RCDATA state + HtmlToken ReadCharacterReferenceInRcData () + { + return ReadCharacterReference (HtmlTokenizerState.RcData); + } + + // 8.2.4.5 RAWTEXT state + HtmlToken ReadRawText () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + break; + } + + c = (char) nc; + + switch (c) { + case '<': + TokenizerState = HtmlTokenizerState.RawTextLessThan; + return EmitDataToken (false, false); + default: + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.RawText); + + return EmitDataToken (false, false); + } + + // 8.2.4.6 Script data state + HtmlToken ReadScriptData () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + break; + } + + c = (char) nc; + + switch (c) { + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataLessThan; + break; + default: + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptData); + + return EmitScriptDataToken (); + } + + // 8.2.4.7 PLAINTEXT state + HtmlToken ReadPlainText () + { + int nc = Read (); + + while (nc != -1) { + char c = (char) nc; + + data.Append (c == '\0' ? '\uFFFD' : c); + nc = Read (); + } + + TokenizerState = HtmlTokenizerState.EndOfFile; + + return EmitDataToken (false, false); + } + + // 8.2.4.8 Tag open state + HtmlToken ReadTagOpen () + { + int nc = Read (); + char c; + + if (nc == -1) { + var token = IgnoreTruncatedTags ? null : CreateDataToken ("<"); + TokenizerState = HtmlTokenizerState.EndOfFile; + return token; + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append ('<'); + data.Append (c); + + switch ((c = (char) nc)) { + case '!': + TokenizerState = HtmlTokenizerState.MarkupDeclarationOpen; + break; + case '?': + TokenizerState = HtmlTokenizerState.BogusComment; + data.Length = 1; + data[0] = c; + break; + case '/': + TokenizerState = HtmlTokenizerState.EndTagOpen; + break; + default: + if (IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.TagName; + isEndTag = false; + name.Append (c); + } else { + TokenizerState = HtmlTokenizerState.Data; + } + break; + } + + return null; + } + + // 8.2.4.9 End tag open state + HtmlToken ReadEndTagOpen () + { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '>': // parse error + TokenizerState = HtmlTokenizerState.Data; + data.Length = 0; // FIXME: this is probably wrong + break; + default: + if (IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.TagName; + isEndTag = true; + name.Append (c); + } else { + TokenizerState = HtmlTokenizerState.BogusComment; + data.Length = 1; + data[0] = c; + } + break; + } + + return null; + } + + // 8.2.4.10 Tag name state + HtmlToken ReadTagName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + break; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + break; + case '>': + tag = CreateTagToken (name.ToString (), isEndTag); + data.Length = 0; + name.Length = 0; + + return EmitTagToken (); + default: + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.TagName); + + tag = CreateTagToken (name.ToString (), isEndTag); + name.Length = 0; + + return null; + } + + // 8.2.4.11 RCDATA less-than sign state + HtmlToken ReadRcDataLessThan () + { + return ReadGenericRawTextLessThan (HtmlTokenizerState.RcData, HtmlTokenizerState.RcDataEndTagOpen); + } + + // 8.2.4.12 RCDATA end tag open state + HtmlToken ReadRcDataEndTagOpen () + { + return ReadGenericRawTextEndTagOpen (DecodeCharacterReferences, HtmlTokenizerState.RcData, HtmlTokenizerState.RcDataEndTagName); + } + + // 8.2.4.13 RCDATA end tag name state + HtmlToken ReadRcDataEndTagName () + { + return ReadGenericRawTextEndTagName (DecodeCharacterReferences, HtmlTokenizerState.RcData); + } + + // 8.2.4.14 RAWTEXT less-than sign state + HtmlToken ReadRawTextLessThan () + { + return ReadGenericRawTextLessThan (HtmlTokenizerState.RawText, HtmlTokenizerState.RawTextEndTagOpen); + } + + // 8.2.4.15 RAWTEXT end tag open state + HtmlToken ReadRawTextEndTagOpen () + { + return ReadGenericRawTextEndTagOpen (false, HtmlTokenizerState.RawText, HtmlTokenizerState.RawTextEndTagName); + } + + // 8.2.4.16 RAWTEXT end tag name state + HtmlToken ReadRawTextEndTagName () + { + return ReadGenericRawTextEndTagName (false, HtmlTokenizerState.RawText); + } + + // 8.2.4.17 Script data less-than sign state + HtmlToken ReadScriptDataLessThan () + { + int nc = Peek (); + + data.Append ('<'); + + switch ((char) nc) { + case '/': + TokenizerState = HtmlTokenizerState.ScriptDataEndTagOpen; + data.Append ('/'); + name.Length = 0; + Read (); + break; + case '!': + TokenizerState = HtmlTokenizerState.ScriptDataEscapeStart; + data.Append ('!'); + Read (); + break; + default: + TokenizerState = HtmlTokenizerState.ScriptData; + break; + } + + return null; + } + + // 8.2.4.18 Script data end tag open state + HtmlToken ReadScriptDataEndTagOpen () + { + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + if (c == 'S' || c == 's') { + TokenizerState = HtmlTokenizerState.ScriptDataEndTagName; + name.Append ('s'); + data.Append (c); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptData; + } + + return null; + } + + // 8.2.4.19 Script data end tag name state + HtmlToken ReadScriptDataEndTagName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitScriptDataToken (); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + if (NameIs ("script")) { + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + break; + } + goto default; + case '/': + if (NameIs ("script")) { + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + break; + } + goto default; + case '>': + if (NameIs ("script")) { + var token = CreateTagToken (name.ToString (), true); + TokenizerState = HtmlTokenizerState.Data; + data.Length = 0; + name.Length = 0; + return token; + } + goto default; + default: + if (!IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.ScriptData; + name.Length = 0; + return null; + } + + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEndTagName); + + tag = CreateTagToken (name.ToString (), true); + name.Length = 0; + + return null; + } + + // 8.2.4.20 Script data escape start state + HtmlToken ReadScriptDataEscapeStart () + { + int nc = Peek (); + + if (nc == '-') { + TokenizerState = HtmlTokenizerState.ScriptDataEscapeStartDash; + data.Append ('-'); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptData; + } + + return null; + } + + // 8.2.4.21 Script data escape start dash state + HtmlToken ReadScriptDataEscapeStartDash () + { + int nc = Peek (); + + if (nc == '-') { + TokenizerState = HtmlTokenizerState.ScriptDataEscapedDashDash; + data.Append ('-'); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptData; + } + + return null; + } + + // 8.2.4.22 Script data escaped state + HtmlToken ReadScriptDataEscaped () + { + HtmlToken token = null; + + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + switch (c) { + case '-': + TokenizerState = HtmlTokenizerState.ScriptDataEscapedDash; + data.Append ('-'); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataEscapedLessThan; + token = EmitScriptDataToken (); + data.Append ('<'); + break; + default: + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEscaped); + + return token; + } + + // 8.2.4.23 Script data escaped dash state + HtmlToken ReadScriptDataEscapedDash () + { + HtmlToken token = null; + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + switch ((c = (char) nc)) { + case '-': + TokenizerState = HtmlTokenizerState.ScriptDataEscapedDashDash; + data.Append ('-'); + Read (); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataEscapedLessThan; + token = EmitScriptDataToken (); + data.Append ('<'); + Read (); + break; + default: + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + + return token; + } + + // 8.2.4.24 Script data escaped dash dash state + HtmlToken ReadScriptDataEscapedDashDash () + { + HtmlToken token = null; + + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + switch (c) { + case '-': + data.Append ('-'); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataEscapedLessThan; + token = EmitScriptDataToken (); + data.Append ('<'); + break; + case '>': + TokenizerState = HtmlTokenizerState.ScriptData; + data.Append ('>'); + break; + default: + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + data.Append (c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEscapedDashDash); + + return token; + } + + // 8.2.4.25 Script data escaped less-than sign state + HtmlToken ReadScriptDataEscapedLessThan () + { + int nc = Peek (); + char c = (char) nc; + + if (c == '/') { + TokenizerState = HtmlTokenizerState.ScriptDataEscapedEndTagOpen; + data.Append (c); + name.Length = 0; + Read (); + } else if (IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapeStart; + data.Append (c); + name.Append (c); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + } + + return null; + } + + // 8.2.4.26 Script data escaped end tag open state + HtmlToken ReadScriptDataEscapedEndTagOpen () + { + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + if (IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.ScriptDataEscapedEndTagName; + data.Append (c); + name.Append (c); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + } + + return null; + } + + // 8.2.4.27 Script data escaped end tag name state + HtmlToken ReadScriptDataEscapedEndTagName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitScriptDataToken (); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + if (NameIs ("script")) { + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + break; + } + + goto default; + case '/': + if (NameIs ("script")) { + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + break; + } + goto default; + case '>': + if (NameIs ("script")) { + var token = CreateTagToken (name.ToString (), true); + TokenizerState = HtmlTokenizerState.Data; + data.Length = 0; + name.Length = 0; + return token; + } + goto default; + default: + if (!IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.ScriptData; + return null; + } + + name.Append (c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEscapedEndTagName); + + tag = CreateTagToken (name.ToString (), true); + name.Length = 0; + + return null; + } + + // 8.2.4.28 Script data double escape start state + HtmlToken ReadScriptDataDoubleEscapeStart () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitScriptDataToken (); + } + + c = (char) nc; + + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': case '/': case '>': + if (NameIs ("script")) + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + else + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + name.Length = 0; + break; + default: + if (!IsAsciiLetter (c)) + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + else + name.Append (c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataDoubleEscapeStart); + + return null; + } + + // 8.2.4.29 Script data double escaped state + HtmlToken ReadScriptDataDoubleEscaped () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + switch (c) { + case '-': + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapedDash; + data.Append ('-'); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapedLessThan; + data.Append ('<'); + break; + default: + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEscaped); + + return null; + } + + // 8.2.4.30 Script data double escaped dash state + HtmlToken ReadScriptDataDoubleEscapedDash () + { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + switch ((c = (char) nc)) { + case '-': + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapedDashDash; + data.Append ('-'); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapedLessThan; + data.Append ('<'); + break; + default: + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + data.Append (c == '\0' ? '\uFFFD' : c); + break; + } + + return null; + } + + // 8.2.4.31 Script data double escaped dash dash state + HtmlToken ReadScriptDataDoubleEscapedDashDash () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitScriptDataToken (); + } + + c = (char) nc; + + switch (c) { + case '-': + data.Append ('-'); + break; + case '<': + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapedLessThan; + data.Append ('<'); + break; + case '>': + TokenizerState = HtmlTokenizerState.ScriptData; + data.Append ('>'); + break; + default: + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + data.Append (c); + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataEscapedDashDash); + + return null; + } + + // 8.2.4.32 Script data double escaped less-than sign state + HtmlToken ReadScriptDataDoubleEscapedLessThan () + { + int nc = Peek (); + + if (nc == '/') { + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscapeEnd; + data.Append ('/'); + Read (); + } else { + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + } + + return null; + } + + // 8.2.4.33 Script data double escape end state + HtmlToken ReadScriptDataDoubleEscapeEnd () + { + do { + int nc = Peek (); + char c = (char) nc; + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': case '/': case '>': + if (NameIs ("script")) + TokenizerState = HtmlTokenizerState.ScriptDataEscaped; + else + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + data.Append (c); + Read (); + break; + default: + if (!IsAsciiLetter (c)) { + TokenizerState = HtmlTokenizerState.ScriptDataDoubleEscaped; + } else { + name.Append (c); + data.Append (c); + Read (); + } + break; + } + } while (TokenizerState == HtmlTokenizerState.ScriptDataDoubleEscapeEnd); + + return null; + } + + // 8.2.4.34 Before attribute name state + HtmlToken ReadBeforeAttributeName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + tag = null; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + break; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + return null; + case '>': + return EmitTagToken (); + case '"': case '\'': case '<': case '=': + // parse error + goto default; + default: + TokenizerState = HtmlTokenizerState.AttributeName; + name.Append (c == '\0' ? '\uFFFD' : c); + return null; + } + } while (true); + } + + // 8.2.4.35 Attribute name state + HtmlToken ReadAttributeName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + tag = null; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + TokenizerState = HtmlTokenizerState.AfterAttributeName; + break; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + break; + case '=': + TokenizerState = HtmlTokenizerState.BeforeAttributeValue; + break; + case '>': + EmitTagAttribute (); + + return EmitTagToken (); + default: + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.AttributeName); + + EmitTagAttribute (); + + return null; + } + + // 8.2.4.36 After attribute name state + HtmlToken ReadAfterAttributeName () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + tag = null; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + break; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + return null; + case '=': + TokenizerState = HtmlTokenizerState.BeforeAttributeValue; + return null; + case '>': + return EmitTagToken (); + case '"': case '\'': case '<': + // parse error + goto default; + default: + TokenizerState = HtmlTokenizerState.AttributeName; + name.Append (c == '\0' ? '\uFFFD' : c); + return null; + } + } while (true); + } + + // 8.2.4.37 Before attribute value state + HtmlToken ReadBeforeAttributeValue () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + tag = null; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + break; + case '"': case '\'': + TokenizerState = HtmlTokenizerState.AttributeValueQuoted; + quote = c; + return null; + case '&': + TokenizerState = HtmlTokenizerState.CharacterReferenceInAttributeValue; + return null; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + return null; + case '>': + return EmitTagToken (); + case '<': case '=': case '`': + // parse error + goto default; + default: + TokenizerState = HtmlTokenizerState.AttributeValueUnquoted; + name.Append (c == '\0' ? '\uFFFD' : c); + return null; + } + } while (true); + } + + // 8.2.4.38 Attribute value (double-quoted) state + HtmlToken ReadAttributeValueQuoted () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '&': + TokenizerState = HtmlTokenizerState.CharacterReferenceInAttributeValue; + return null; + default: + if (c == quote) { + TokenizerState = HtmlTokenizerState.AfterAttributeValueQuoted; + quote = '\0'; + break; + } + + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.AttributeValueQuoted); + + attribute.Value = name.ToString (); + name.Length = 0; + + return null; + } + + // 8.2.4.40 Attribute value (unquoted) state + HtmlToken ReadAttributeValueUnquoted () + { + do { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + break; + case '&': + TokenizerState = HtmlTokenizerState.CharacterReferenceInAttributeValue; + return null; + case '>': + attribute.Value = name.ToString (); + name.Length = 0; + + return EmitTagToken (); + case '\'': case '<': case '=': case '`': + // parse error + goto default; + default: + name.Append (c == '\0' ? '\uFFFD' : c); + break; + } + } while (TokenizerState == HtmlTokenizerState.AttributeValueUnquoted); + + attribute.Value = name.ToString (); + name.Length = 0; + + return null; + } + + // 8.2.4.41 Character reference in attribute value state + HtmlToken ReadCharacterReferenceInAttributeValue () + { + char additionalAllowedCharacter = quote == '\0' ? '>' : quote; + int nc = Peek (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + name.Length = 0; + + return EmitDataToken (false, true); + } + + c = (char) nc; + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': case '<': case '&': + // no character is consumed, emit '&' + name.Append ('&'); + break; + default: + if (c == additionalAllowedCharacter) { + // this is not a character reference, nothing is consumed + name.Append ('&'); + break; + } + + entity.Push ('&'); + + while (entity.Push (c)) { + Read (); + + if (c == ';') + break; + + if ((nc = Peek ()) == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + data.Length--; + data.Append (entity.GetPushedInput ()); + entity.Reset (); + + return EmitDataToken (false, true); + } + + c = (char) nc; + } + + var pushed = entity.GetPushedInput (); + string value; + + if (c == '=' || IsAlphaNumeric (c)) + value = pushed; + else + value = entity.GetValue (); + + data.Length--; + data.Append (pushed); + name.Append (value); + entity.Reset (); + break; + } + + if (quote == '\0') + TokenizerState = HtmlTokenizerState.AttributeValueUnquoted; + else + TokenizerState = HtmlTokenizerState.AttributeValueQuoted; + + return null; + } + + // 8.2.4.42 After attribute value (quoted) state + HtmlToken ReadAfterAttributeValueQuoted () + { + HtmlToken token = null; + int nc = Peek (); + bool consume; + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitDataToken (false, true); + } + + c = (char) nc; + + switch (c) { + case '\t': case '\r': case '\n': case '\f': case ' ': + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + data.Append (c); + consume = true; + break; + case '/': + TokenizerState = HtmlTokenizerState.SelfClosingStartTag; + data.Append (c); + consume = true; + break; + case '>': + token = EmitTagToken (); + consume = true; + break; + default: + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + consume = false; + break; + } + + if (consume) + Read (); + + return token; + } + + // 8.2.4.43 Self-closing start tag state + HtmlToken ReadSelfClosingStartTag () + { + int nc = Read (); + char c; + + if (nc == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitDataToken (false, true); + } + + c = (char) nc; + + if (c == '>') { + tag.IsEmptyElement = true; + + return EmitTagToken (); + } + + // parse error + TokenizerState = HtmlTokenizerState.BeforeAttributeName; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + + return null; + } + + // 8.2.4.44 Bogus comment state + HtmlToken ReadBogusComment () + { + int nc; + char c; + + do { + if ((nc = Read ()) == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + break; + } + + if ((c = (char) nc) == '>') + break; + + data.Append (c == '\0' ? '\uFFFD' : c); + } while (true); + + TokenizerState = HtmlTokenizerState.Data; + + return EmitCommentToken (data, true); + } + + // 8.2.4.45 Markup declaration open state + HtmlToken ReadMarkupDeclarationOpen () + { + int count = 0, nc; + char c = '\0'; + + while (count < 2) { + if ((nc = Peek ()) == -1) { + TokenizerState = HtmlTokenizerState.EndOfFile; + return EmitDataToken (false, true); + } + + if ((c = (char) nc) != '-') + break; + + // Note: we save the data in case we hit a parse error and have to emit a data token + data.Append (c); + Read (); + count++; + } + + if (count == 2) { + // "