├── .swift-version ├── docs ├── img │ ├── gh.png │ ├── dash.png │ ├── carat.png │ └── spinner.gif ├── docsets │ ├── SwiftSMTP.tgz │ └── SwiftSMTP.docset │ │ └── Contents │ │ ├── Resources │ │ ├── docSet.dsidx │ │ └── Documents │ │ │ ├── img │ │ │ ├── gh.png │ │ │ ├── carat.png │ │ │ ├── dash.png │ │ │ └── spinner.gif │ │ │ ├── js │ │ │ ├── jazzy.js │ │ │ └── jazzy.search.js │ │ │ ├── css │ │ │ ├── highlight.css │ │ │ └── jazzy.css │ │ │ ├── Enums.html │ │ │ └── Typealiases.html │ │ └── Info.plist ├── undocumented.json ├── badge.svg ├── js │ ├── jazzy.js │ └── jazzy.search.js ├── css │ ├── highlight.css │ └── jazzy.css ├── Enums.html └── Typealiases.html ├── Assets └── swift-smtp-bird.png ├── Tests ├── SwiftSMTPTests │ ├── x.png │ ├── cert.pfx │ ├── cert.pem │ ├── key.pem │ ├── TestAuthEncoder.swift │ ├── TestAttachment.swift │ ├── TestTLSMode.swift │ ├── TestDataSender.swift │ ├── TestSMTPSocket.swift │ ├── TestMiscellaneous.swift │ ├── TestMailSender.swift │ └── Constant.swift └── LinuxMain.swift ├── .gitignore ├── .swiftlint.yml ├── .jazzy.yaml ├── Package.swift ├── .github ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── Sources └── SwiftSMTP │ ├── AuthMethod.swift │ ├── Common.swift │ ├── Response.swift │ ├── AuthEncoder.swift │ ├── Command.swift │ ├── SMTPError.swift │ ├── MailSender.swift │ ├── TLSConfiguration.swift │ ├── SMTP.swift │ ├── Mail.swift │ ├── SMTPSocket.swift │ ├── DataSender.swift │ └── Attachment.swift ├── .travis.yml ├── migration-guide.md ├── README.md └── LICENSE.txt /.swift-version: -------------------------------------------------------------------------------- 1 | 5.1 2 | -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/img/spinner.gif -------------------------------------------------------------------------------- /Assets/swift-smtp-bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/Assets/swift-smtp-bird.png -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/Tests/SwiftSMTPTests/x.png -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.tgz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | build 4 | /Packages 5 | Package.resolved 6 | /*.xcodeproj 7 | .swiftpm 8 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/cert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/Tests/SwiftSMTPTests/cert.pfx -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/dannys/projects/kitura/Swift-SMTP" 6 | } -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | excluded: 5 | - Packages 6 | disabled_rules: 7 | - trailing_whitespace 8 | - line_length 9 | - identifier_name -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kitura/Swift-SMTP/HEAD/docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: SwiftSMTP 2 | author: IBM and Kitura project contributors 3 | github_url: https://github.com/Kitura/Swift-SMTP/ 4 | 5 | theme: fullwidth 6 | clean: true 7 | exclude: [Packages] 8 | 9 | readme: README.md 10 | 11 | skip_undocumented: false 12 | hide_documentation_coverage: false 13 | 14 | xcodebuild_arguments: [] 15 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.swiftsmtp 7 | CFBundleName 8 | SwiftSMTP 9 | DocSetPlatformFamily 10 | swiftsmtp 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftSMTP", 7 | products: [ 8 | .library( 9 | name: "SwiftSMTP", 10 | targets: ["SwiftSMTP"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/Kitura/BlueSocket.git", from: "2.0.2"), 14 | .package(url: "https://github.com/Kitura/BlueSSLService.git", from: "2.0.1"), 15 | .package(url: "https://github.com/Kitura/BlueCryptor.git", from: "2.0.1"), 16 | .package(url: "https://github.com/Kitura/LoggerAPI.git", from: "1.9.200"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SwiftSMTP", 21 | dependencies: ["Socket", "SSLService", "Cryptor", "LoggerAPI"]), 22 | .testTarget( 23 | name: "SwiftSMTPTests", 24 | dependencies: ["SwiftSMTP"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Checklist: 16 | 17 | 18 | - [ ] I have submitted a [CLA form](https://github.com/IBM-Swift/CLA) 19 | - [ ] If applicable, I have updated the documentation accordingly. 20 | - [ ] If applicable, I have added tests to cover my changes. 21 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/AuthMethod.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2018 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | /// Supported authentication methods for logging into the SMTP server. 20 | public enum AuthMethod: String { 21 | /// CRAM-MD5 authentication. 22 | case cramMD5 = "CRAM-MD5" 23 | /// LOGIN authentication. 24 | case login = "LOGIN" 25 | /// PLAIN authentication. 26 | case plain = "PLAIN" 27 | /// XOAUTH2 authentication. Requires a valid access token. 28 | case xoauth2 = "XOAUTH2" 29 | } 30 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis CI build file. 2 | 3 | # whitelist (branches that should be built) 4 | branches: 5 | only: 6 | - master 7 | - /^issue.*$/ 8 | 9 | # the matrix of builds should cover each combination of Swift version 10 | # and platform that is supported. The version of Swift used is specified 11 | # by .swift-version, unless SWIFT_SNAPSHOT is specified. 12 | matrix: 13 | include: 14 | - os: linux 15 | dist: bionic 16 | sudo: required 17 | services: docker 18 | env: DOCKER_IMAGE=docker.kitura.net/kitura/swift-ci-ubuntu18.04:5.2.5 DOCKER_ENVIRONMENT="EMAIL PASSWORD" 19 | - os: linux 20 | dist: focal 21 | sudo: required 22 | services: docker 23 | env: DOCKER_IMAGE=docker.kitura.net/kitura/swift-ci-ubuntu20.04:5.4 DOCKER_ENVIRONMENT="EMAIL PASSWORD" 24 | - os: linux 25 | dist: focal 26 | sudo: required 27 | services: docker 28 | env: DOCKER_IMAGE=docker.kitura.net/kitura/swift-ci-ubuntu20.04:latest USE_SWIFT_DEVELOPMENT_SNAPSHOT=1 DOCKER_ENVIRONMENT="EMAIL PASSWORD" 29 | - os: osx 30 | osx_image: xcode12.2 31 | sudo: required 32 | env: JAZZY_ELIGIBLE=true 33 | - os: osx 34 | osx_image: xcode13.2 35 | sudo: required 36 | env: USE_SWIFT_DEVELOPMENT_SNAPSHOT=1 37 | 38 | before_install: 39 | - git clone https://github.com/Kitura/Package-Builder.git 40 | 41 | script: 42 | - ./Package-Builder/build-package.sh -projectDir $TRAVIS_BUILD_DIR 43 | 44 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to IBM-Swift 2 | 3 | We welcome contributions, and request you follow these guidelines. 4 | 5 | - [Raising issues](#raising-issues) 6 | - [Contributor License Agreement](#contributor-license-agreement) 7 | - [Coding Standards](#coding-standards) 8 | 9 | 10 | ## Raising issues 11 | 12 | Please raise any bug reports on the issue tracker. Be sure to 13 | search the list to see if your issue has already been raised. 14 | 15 | A good bug report is one that makes it easy for us to understand what you were 16 | trying to do and what went wrong. Provide as much context as possible so we can try to recreate the issue. 17 | 18 | ### Contributor License Agreement 19 | 20 | In order for us to accept pull-requests, the contributor must first complete 21 | a Contributor License Agreement (CLA). Please see our [CLA repo](http://github.com/IBM-Swift/CLA) for more information. 22 | 23 | This clarifies the intellectual property license granted with any contribution. It is for your protection as a 24 | Contributor as well as the protection of IBM and its customers; it does not 25 | change your rights to use your own Contributions for any other purpose. 26 | 27 | ### Coding standards 28 | 29 | Please ensure you follow [the Kitura coding standards](https://github.com/IBM-Swift/Kitura/blob/master/Documentation/CodeConventions.md) 30 | 31 | Please note: 32 | 33 | - all files must have the Apache license in the header. 34 | - all PRs must have passing builds for all operating systems. 35 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/Common.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | enum Result { 20 | case success(T) 21 | case failure(Error) 22 | } 23 | 24 | let cache = NSCache() 25 | let CRLF = "\r\n" 26 | 27 | extension String { 28 | var base64Encoded: String { 29 | return Data(utf8).base64EncodedString() 30 | } 31 | var base64EncodedWithLineLimit: String { 32 | return Data(utf8).base64EncodedString(options: .lineLength76Characters) 33 | } 34 | 35 | var mimeEncoded: String? { 36 | guard let encoded = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { 37 | return nil 38 | } 39 | let quoted = encoded 40 | .replacingOccurrences(of: "%20", with: "_") 41 | .replacingOccurrences(of: ",", with: "%2C") 42 | .replacingOccurrences(of: "%", with: "=") 43 | return "=?UTF-8?Q?\(quoted)?=" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/Response.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | struct Response { 20 | let code: ResponseCode 21 | let message: String 22 | let response: String 23 | 24 | init(code: ResponseCode, message: String, response: String) { 25 | self.code = code 26 | self.message = message 27 | self.response = response 28 | } 29 | } 30 | 31 | struct ResponseCode: Equatable { 32 | let rawValue: Int 33 | init(_ value: Int) { rawValue = value } 34 | 35 | static let serviceReady = ResponseCode(220) 36 | static let connectionClosing = ResponseCode(221) 37 | static let authSucceeded = ResponseCode(235) 38 | static let commandOK = ResponseCode(250) 39 | static let willForward = ResponseCode(251) 40 | static let containingChallenge = ResponseCode(334) 41 | static let startMailInput = ResponseCode(354) 42 | 43 | public static func==(lhs: ResponseCode, rhs: ResponseCode) -> Bool { 44 | return lhs.rawValue == rhs.rawValue 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEXjCCA0agAwIBAgIJALlhKzgHboVMMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV 3 | BAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEPMA0GA1UEBxMGQXVzdGluMQwwCgYDVQQK 4 | EwNJQk0xDzANBgNVBAsTBktpdHVyYTENMAsGA1UEAxMEUXVhbjEeMBwGCSqGSIb3 5 | DQEJARYPcXVhbi52b0BpYm0uY29tMB4XDTE3MDMyNzIzNDYyN1oXDTE4MDMyNzIz 6 | NDYyN1owfDELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMQ8wDQYDVQQHEwZB 7 | dXN0aW4xDDAKBgNVBAoTA0lCTTEPMA0GA1UECxMGS2l0dXJhMQ0wCwYDVQQDEwRR 8 | dWFuMR4wHAYJKoZIhvcNAQkBFg9xdWFuLnZvQGlibS5jb20wggEiMA0GCSqGSIb3 9 | DQEBAQUAA4IBDwAwggEKAoIBAQDqa+REt+3M759JoMmk6nSW2MJuy2Pt+1vGF5VA 10 | eRfCl20Tb6AL0oRogP0WxTC7W4edpx62J4pChtFObMs3WiswICoyLpYBjkVAIhdp 11 | H07Rhgbt+kd7AOef8W3AaGRLLN6wPT/gD5gmJlKYtD/poyXW70STQi+G1TActNhl 12 | ES8WYpphj4u2EdlrknjfxDFnuKJ5KGPrUZoB50WoUCYFTi7olcH4GsAJHscjIFu1 13 | huN5fCLycJ1nA/Em4pvz5+s8U0QWPqdFgkeBlt9EClinkAATp+dcg0VX5SL9rVnb 14 | EApb3tbQ2P94pF9ggnHut/oJf2f4qXo+reaw3znJacWGDv1DAgMBAAGjgeIwgd8w 15 | HQYDVR0OBBYEFJACyupTuRVvykzDLnVeWw296MIaMIGvBgNVHSMEgacwgaSAFJAC 16 | yupTuRVvykzDLnVeWw296MIaoYGApH4wfDELMAkGA1UEBhMCVVMxDjAMBgNVBAgT 17 | BVRleGFzMQ8wDQYDVQQHEwZBdXN0aW4xDDAKBgNVBAoTA0lCTTEPMA0GA1UECxMG 18 | S2l0dXJhMQ0wCwYDVQQDEwRRdWFuMR4wHAYJKoZIhvcNAQkBFg9xdWFuLnZvQGli 19 | bS5jb22CCQC5YSs4B26FTDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB 20 | AQCFDJUw2I4wGDWSabNr9GohmEFjvp25QdhuEviGkfdsjk3RYebsJ6wRCzY+ldEs 21 | 0j482EwTY7NnnWd3uGaYsHKNT2ew3DfVxb1FNGPB/M+xrOWdT4eZlVK3BkfMXvRd 22 | DQYDqjkBpB8qRnrek8cg7Tk49S4T03m0nr/mQyP3zE6bP3qQxOnlVbV/x6/g7VGT 23 | xM0i98oC66arTtGf2uqwl6QOL2QHSg4/PMIpDC0Y7bGeoT+2vcyxRS30gLSAWbrq 24 | VMbJKbLJlOuJw9ql/0X9eQC4qt91zLLxRZjYgYYvGxFXo71bX6EHM1bWEAYg9Lu2 25 | y6XofWQxWD+1Si+7bRCkL+CS 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA6mvkRLftzO+fSaDJpOp0ltjCbstj7ftbxheVQHkXwpdtE2+g 3 | C9KEaID9FsUwu1uHnacetieKQobRTmzLN1orMCAqMi6WAY5FQCIXaR9O0YYG7fpH 4 | ewDnn/FtwGhkSyzesD0/4A+YJiZSmLQ/6aMl1u9Ek0IvhtUwHLTYZREvFmKaYY+L 5 | thHZa5J438QxZ7iieShj61GaAedFqFAmBU4u6JXB+BrACR7HIyBbtYbjeXwi8nCd 6 | ZwPxJuKb8+frPFNEFj6nRYJHgZbfRApYp5AAE6fnXINFV+Ui/a1Z2xAKW97W0Nj/ 7 | eKRfYIJx7rf6CX9n+Kl6Pq3msN85yWnFhg79QwIDAQABAoIBAQC/HKijkWOwQOaW 8 | ixv5dB8K37pbwzs7yEGAlLdcMZy6SuNlBgrvuHe0DvzGdIqPJEbCs31pOYERTYIU 9 | MsPV44/0EzTzZmFq8UbpyyFU1W5XiLHbj8B4ujsbfSNhynmBhBokijqp+2yqJXIP 10 | BlxYqGZv/O7mMv42KVWpAZKtir3du4P0pT7xWTHilTd0DbwIBBiJoCfAW+78gtA9 11 | nLdleIJCXFOLIETicGbF6raPTSu7Z0an1/XVLfoFcuoOzEZoyYIAb5ozMq1WF4cv 12 | uI+LCI5EnOer8GyMCBG66aFELGfdFRUI7tRdg98thy7KJ8wRPpfMyh9UcUhN0s1g 13 | aff6YIJhAoGBAPfaQ7RQMn+v27SzFGh97+puq9pHJenmvUX6DthDsr3Uf9+nPIFg 14 | LIeP5LxzXpFZubK+2kwNWWoi6AV1pYPRsauZ3k+E4zAhXtF2pZivC32Hue1XNztq 15 | aW7ng/ndXFY7jQNNWYazRyrF3CaMl15TGYuSdNh7cygn2sklsqHKhHpzAoGBAPIg 16 | meGIoO/CDW7d+Pke6RnzKw2fdRmk7qsACPYoriwwMaByPvgEUSfNrlEE/eDcsHwP 17 | RmOcEPsnAlaCmUuMpo5Gj6jfM2bj7P7GeJ+VM22nv2cfKlVtrxP7WhuEmwXdg2lz 18 | d29ltk2ajov3DRAjfrplyMlO5CaCGlOAfzUnCK3xAoGBAOpjne1yfh8klrivNhiP 19 | KIjh+mElMaSeUdZQYSOB+hHtWLSQOfb7lYDpwl25GPCKEsQIGvcbFLj7o8It/MXJ 20 | U6U9kPBQcm080ady9a2LtGkVJu5dsVzeCDEafkOYZE8kZ/l8d7Kb7ix0Cvrlr+xC 21 | 2ACXEyr6q++IqS3aGbFJjLkjAoGBAJib+cGQVzenDMZzPAjw9aU4gktc1Pbr4M6B 22 | ACT+8QDDA5SITa4PMoOu/Q7t4YLINqiLDCeeZ4mVRcD3Id3fcd89FDExNXnFcUwI 23 | FmEnLjoQP/CkUQ91SaODioDLrNYej0R41a+t4SC6qNwJQ/+HD8o2ez5+7ghjempl 24 | FEiRKMRRAoGANz/qPSFEJkkusexm5iit+A/lS1hUjhqfMcsThh7nwOIOgRGKGAko 25 | yk/6+qzYpLuFtnd/n0FKBx1ZoH2bIU+v7c3Q0stlorWi4SSDmhEwU2C5TYgE5f7t 26 | e8+gFHGjJF306QP6KKqpfDwjkuWLzdTLGEl4XZD/DZFIkRuI7AxzPLA= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | import Glibc 19 | @testable import SwiftSMTPTests 20 | 21 | srand(UInt32(time(nil))) 22 | 23 | // http://stackoverflow.com/questions/24026510/how-do-i-shuffle-an-array-in-swift 24 | 25 | extension MutableCollection { 26 | mutating func shuffle() { 27 | let c = count 28 | guard c > 1 else { return } 29 | 30 | for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) { 31 | let d: Int = numericCast(random() % numericCast(unshuffledCount)) 32 | guard d != 0 else { continue } 33 | let i = index(firstUnshuffled, offsetBy: d) 34 | self.swapAt(firstUnshuffled, i) 35 | } 36 | } 37 | } 38 | 39 | extension Sequence { 40 | func shuffled() -> [Iterator.Element] { 41 | var result = Array(self) 42 | result.shuffle() 43 | return result 44 | } 45 | } 46 | 47 | XCTMain([ 48 | testCase(TestAttachment.allTests.shuffled()), 49 | testCase(TestAuthEncoder.allTests.shuffled()), 50 | testCase(TestDataSender.allTests.shuffled()), 51 | testCase(TestMailSender.allTests.shuffled()), 52 | testCase(TestMiscellaneous.allTests.shuffled()), 53 | testCase(TestSMTPSocket.allTests.shuffled()), 54 | testCase(TestTLSMode.allTests.shuffled())].shuffled()) 55 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/AuthEncoder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import Cryptor 19 | 20 | struct AuthEncoder { 21 | static func cramMD5(challenge: String, user: String, password: String) throws -> String { 22 | guard let hmac = HMAC( 23 | using: HMAC.Algorithm.md5, 24 | key: password).update(string: try challenge.base64Decoded())?.final() else { 25 | throw SMTPError.md5HashChallengeFail 26 | } 27 | let digest = CryptoUtils.hexString(from: hmac) 28 | return ("\(user) \(digest)").base64Encoded 29 | } 30 | 31 | static func login(user: String, password: String) -> (encodedUser: String, encodedPassword: String) { 32 | return (user.base64Encoded, password.base64Encoded) 33 | } 34 | 35 | static func plain(user: String, password: String) -> String { 36 | let text = "\u{0000}\(user)\u{0000}\(password)" 37 | return text.base64Encoded 38 | } 39 | 40 | static func xoauth2(user: String, accessToken: String) -> String { 41 | let text = "user=\(user)\u{0001}auth=Bearer \(accessToken)\u{0001}\u{0001}" 42 | return text.base64Encoded 43 | } 44 | } 45 | 46 | extension String { 47 | func base64Decoded() throws -> String { 48 | guard let data = Data(base64Encoded: self, options: .ignoreUnknownCharacters), 49 | let base64Decoded = String(data: data, encoding: .utf8) else { 50 | throw SMTPError.base64DecodeFail(string: self) 51 | } 52 | return base64Decoded 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/Command.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | enum Command { 20 | case connect 21 | case ehlo(String) 22 | case helo(String) 23 | case starttls 24 | case auth(AuthMethod, String?) 25 | case authUser(String) 26 | case authPassword(String) 27 | case mail(String) 28 | case rcpt(String) 29 | case data 30 | case dataEnd 31 | case quit 32 | 33 | var text: String { 34 | switch self { 35 | case .connect: return "" 36 | case .ehlo(let domain): return "EHLO \(domain)" 37 | case .helo(let domain): return "HELO \(domain)" 38 | case .starttls: return "STARTTLS" 39 | case .auth(let method, let credentials): 40 | if let credentials = credentials { 41 | return "AUTH \(method.rawValue) \(credentials)" 42 | } else { 43 | return "AUTH \(method.rawValue)" 44 | } 45 | case .authUser(let user): return user 46 | case .authPassword(let password): return password 47 | case .mail(let from): return "MAIL FROM:<\(from)>" 48 | case .rcpt(let to): return "RCPT TO:<\(to)>" 49 | case .data: return "DATA" 50 | case .dataEnd: return "\(CRLF)." 51 | case .quit: return "QUIT" 52 | } 53 | } 54 | 55 | var expectedResponseCodes: [ResponseCode] { 56 | switch self { 57 | case .connect: return [.serviceReady] 58 | case .starttls: return [.serviceReady] 59 | case .auth(let method, _): 60 | switch method { 61 | case .cramMD5: return [.containingChallenge] 62 | case .login: return [.containingChallenge] 63 | case .plain: return [.authSucceeded] 64 | case .xoauth2: return [.authSucceeded] 65 | } 66 | case .authUser: return [.containingChallenge] 67 | case .authPassword: return [.authSucceeded] 68 | case .rcpt: return [.commandOK, .willForward] 69 | case .data: return [.startMailInput] 70 | case .quit: return [.connectionClosing, .commandOK] 71 | default: return [.commandOK] 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /migration-guide.md: -------------------------------------------------------------------------------- 1 | # 5.0.0 Migration Guide 2 | 3 | - The default port is now `587`. 4 | 5 | - The `SMTP` struct is now initialized with a `TLSMode` enum instead of a `useTLS` Bool, allowing more configuration options: 6 | 7 | ```swift 8 | /// TLSMode enum for what form of connection security to enforce. 9 | public enum TLSMode { 10 | /// Upgrades the connection to TLS if STARTLS command is received, else sends mail without security. 11 | case normal 12 | 13 | /// Send mail over plaintext and ignore STARTTLS commands and TLS options. Could throw an error if server requires TLS. 14 | case ignoreTLS 15 | 16 | /// Only send mail after an initial successful TLS connection. Connection will fail if a TLS connection cannot be established. The default port, 587, will likely need to be adjusted depending on your server. 17 | case requireTLS 18 | 19 | /// Expect a STARTTLS command from the server and require the connection is upgraded to TLS. Will throw if the server does not issue a STARTTLS command. 20 | case requireSTARTTLS 21 | } 22 | ``` 23 | 24 | # 4.0.0 Migration Guide 25 | 26 | - `User` struct now nested in `Mail` struct to avoid namespace issues [(69)](https://github.com/Kitura/Swift-SMTP/pull/69). Create a user like so: 27 | 28 | ```swift 29 | let sender = Mail.User(name: "Sloth", email: "sloth@gmail.com") 30 | ``` 31 | 32 | - `User` properties are now public 33 | 34 | - The optional `accessToken` parameter of the `SMTP` struct has been removed. If you are using the authorization method `XOAUTH2`, pass in your access token in the `password` parameter instead. For example: 35 | 36 | ```swift 37 | let smtp = SMTP( 38 | hostname: "smtp.gmail.com", 39 | email: "example@gmail.com", 40 | password: "accessToken", 41 | authMethods: [.xoauth2] 42 | ) 43 | ``` 44 | 45 | - Fixed a bug where the wrong `Attachment` was used an an alternative to text content when a `Mail` was initialized with multiple `Attachment`s [(67)](https://github.com/Kitura/Swift-SMTP/pull/67) 46 | 47 | # 3.0.0 Migration Guide 48 | 49 | ## Initialize `SMTP` 50 | 51 | Before `3.0.0`: 52 | 53 | ```swift 54 | public init(hostname: String, 55 | email: String, 56 | password: String, 57 | port: Port = Ports.tls.rawValue, 58 | ssl: SSL? = nil, 59 | authMethods: [AuthMethod] = [], 60 | domainName: String = "localhost", 61 | accessToken: String? = nil, 62 | timeout: UInt = 10) 63 | ``` 64 | 65 | After `3.0.0`: 66 | 67 | ```swift 68 | public init(hostname: String, 69 | email: String, 70 | password: String, 71 | port: Int32 = 465, 72 | useTLS: Bool = true, 73 | tlsConfiguration: TLSConfiguration? = nil, 74 | authMethods: [AuthMethod] = [], 75 | accessToken: String? = nil, 76 | domainName: String = "localhost", 77 | timeout: UInt = 10) 78 | ``` 79 | 80 | ## Renamed 81 | 82 | - `SSL` renamed to `TLSConfiguration` 83 | 84 | ## Removed 85 | 86 | - `Port` typealias 87 | 88 | ## Other Changes 89 | 90 | - `Mail` properties are now public 91 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/SMTPError.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import LoggerAPI 19 | 20 | /// Error type for SwiftSMTP. 21 | public enum SMTPError: Error, CustomStringConvertible { 22 | // AuthCredentials 23 | /// Error decoding string. 24 | case base64DecodeFail(string: String) 25 | 26 | /// Hashing server challenge with MD5 algorithm failed. 27 | case md5HashChallengeFail 28 | 29 | // DataSender 30 | /// File not found at path while trying to send file `Attachment`. 31 | case fileNotFound(path: String) 32 | 33 | /// The preferred `AuthMethod`s could not be found, or your server is sending back a STARTTLS command and requires a connection upgrade. 34 | case noAuthMethodsOrRequiresTLS(hostname: String) 35 | 36 | // Sender 37 | /// Mail has no recipients. 38 | case noRecipients 39 | 40 | /// Failed to create RegularExpression that can check if an email is valid. 41 | case createEmailRegexFailed 42 | 43 | // SMTPSocket 44 | /// Bad response received for command. 45 | case badResponse(command: String, response: String) 46 | 47 | /// Error converting Data read from socket to a String. 48 | case convertDataUTF8Fail(data: Data) 49 | 50 | // User 51 | /// Invalid email provided for `User`. 52 | case invalidEmail(email: String) 53 | 54 | /// STARTTLS was required but the server did not request it. 55 | case requiredSTARTTLS 56 | 57 | /// Description of the `SMTPError`. 58 | public var description: String { 59 | switch self { 60 | case .base64DecodeFail(let s): return "Error decoding string: \(s)." 61 | case .md5HashChallengeFail: return "Hashing server challenge with MD5 algorithm failed." 62 | case .fileNotFound(let p): return "File not found at path while trying to send file `Attachment`: \(p)." 63 | case .noAuthMethodsOrRequiresTLS(let hostname): return "The preferred authorization methods could not be found on \(hostname), or your server is sending back a STARTTLS command and requires a connection upgrade." 64 | case .noRecipients: return "An email requires at least one recipient." 65 | case .createEmailRegexFailed: return "Failed to create RegularExpression that can check if an email is valid." 66 | case .badResponse(let command, let response): return "Bad response received for command. command: (\(command)), response: \(response)" 67 | case .convertDataUTF8Fail(let buf): return "Error converting Data read from socket to a String: \(buf)." 68 | case .invalidEmail(let email): return "Invalid email provided for User: \(email)." 69 | case .requiredSTARTTLS: return "STARTTLS was required but the server did not issue a STARTTLS command." 70 | } 71 | } 72 | 73 | init(_ error: SMTPError) { 74 | self = error 75 | Log.error(description) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestAuthEncoder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | class TestAuthEncoder: XCTestCase { 21 | static var allTests = [ 22 | ("testBase64Decoder", testBase64Decoder), 23 | ("testCramMD5", testCramMD5), 24 | ("testLogin", testLogin), 25 | ("testPlain", testPlain), 26 | ("testXOAuth2", testXOAuth2) 27 | ] 28 | 29 | func testBase64Decoder() throws { 30 | let randomText1Decoded = try randomText1Encoded.base64Decoded() 31 | XCTAssertEqual(randomText1Decoded, randomText1, "result: \(randomText1Decoded) != expected: \(randomText1)") 32 | 33 | let randomText2Decoded = try randomText2Encoded.base64Decoded() 34 | XCTAssertEqual(randomText2Decoded, randomText2, "result: \(randomText2Decoded) != expected: \(randomText2)") 35 | 36 | let randomText3Decoded = try randomText3Encoded.base64Decoded() 37 | XCTAssertEqual(randomText3Decoded, randomText3, "result: \(randomText3Decoded) != expected: \(randomText3)") 38 | } 39 | 40 | func testCramMD5() throws { 41 | let user = "foo@bar.com" 42 | let password = "password" 43 | let challenge = "aGVsbG8=" 44 | 45 | // http://busylog.net/cram-md5-online-generator/ 46 | let expected = "Zm9vQGJhci5jb20gMjhmOGNhMDI0YjBlNjE4YWUzNWQ0NmRiODExNzU2NjM=" 47 | let result = try AuthEncoder.cramMD5(challenge: challenge, user: user, password: password) 48 | XCTAssertEqual(result, expected, "result: \(result) != expected: \(expected)") 49 | } 50 | 51 | func testLogin() { 52 | let user = "foo@bar.com" 53 | let password = "password" 54 | 55 | // https://www.base64decode.org/ 56 | let expected = ("Zm9vQGJhci5jb20=", "cGFzc3dvcmQ=") 57 | let result = AuthEncoder.login(user: user, password: password) 58 | XCTAssertEqual(result.encodedUser, expected.0, "result: \(result.encodedUser) != expected: \(expected.0)") 59 | XCTAssertEqual(result.encodedPassword, expected.1, "result: \(result.encodedPassword) != expected: \(expected.1)") 60 | } 61 | 62 | func testPlain() { 63 | let user = "test" 64 | let password = "testpass" 65 | 66 | // echo -ne "\0foo@bar.com\0password"|base64 67 | let expected = "AHRlc3QAdGVzdHBhc3M=" 68 | let result = AuthEncoder.plain(user: user, password: password) 69 | XCTAssertEqual(result, expected, "result: \(result) != expected: \(expected)") 70 | } 71 | 72 | func testXOAuth2() { 73 | let user = "foo@bar.com" 74 | let token = "token" 75 | 76 | // echo -ne "user=foo@bar.com\001auth=Bearer token\001\001"|base64 77 | let expected = "dXNlcj1mb29AYmFyLmNvbQFhdXRoPUJlYXJlciB0b2tlbgEB" 78 | let result = AuthEncoder.xoauth2(user: user, accessToken: token) 79 | XCTAssertEqual(result, expected, "result: \(result) != expected: \(expected)") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestAttachment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | class TestAttachment: XCTestCase { 21 | static var allTests: [(String, (TestAttachment) -> () throws -> Void)] { 22 | return [ 23 | ("testDataAttachmentHeaders", testDataAttachmentHeaders), 24 | ("testFileAttachmentHeaders", testFileAttachmentHeaders), 25 | ("testHTMLAttachmentHeaders", testHTMLAttachmentHeaders), 26 | ("testGetAlternativeAttachment", testGetAlternativeAttachment) 27 | ] 28 | } 29 | 30 | func testDataAttachmentHeaders() { 31 | let data = "{\"key\": \"hello world\"}".data(using: .utf8)! 32 | let headers = Attachment(data: data, mime: "application/json", name: "file.json").headersString 33 | XCTAssert(headers.contains("CONTENT-TYPE: application/json")) 34 | XCTAssert(headers.contains("CONTENT-DISPOSITION: attachment; filename=\"=?UTF-8?Q?file.json?=\"")) 35 | XCTAssert(headers.contains("CONTENT-TRANSFER-ENCODING: BASE64")) 36 | } 37 | 38 | func testFileAttachmentHeaders() { 39 | let headers = Attachment(filePath: imgFilePath, additionalHeaders: ["CONTENT-ID": "megaman-pic"]).headersString 40 | XCTAssert(headers.contains("CONTENT-DISPOSITION: attachment; filename=\"=?UTF-8?Q?x.png?=\"")) 41 | XCTAssert(headers.contains("CONTENT-TRANSFER-ENCODING: BASE64")) 42 | XCTAssert(headers.contains("CONTENT-ID: megaman-pic")) 43 | XCTAssert(headers.contains("CONTENT-TYPE: application/octet-stream")) 44 | } 45 | 46 | func testHTMLAttachmentHeaders() { 47 | let headers = Attachment(htmlContent: html).headersString 48 | XCTAssert(headers.contains("CONTENT-TYPE: text/html; charset=utf-8")) 49 | XCTAssert(headers.contains("CONTENT-DISPOSITION: inline")) 50 | XCTAssert(headers.contains("CONTENT-TRANSFER-ENCODING: BASE64")) 51 | } 52 | 53 | func testGetAlternativeAttachment() { 54 | let data = "{\"key\": \"hello world\"}".data(using: .utf8)! 55 | let imgAttachment = Attachment(filePath: imgFilePath, additionalHeaders: ["CONTENT-ID": "megaman-pic"]) 56 | let htmlAttachment1 = Attachment(htmlContent: "\(text)", relatedAttachments: [imgAttachment]) 57 | let jsonAttachment = Attachment(data: data, mime: "application/json", name: "file.json") 58 | let htmlAttachment2 = Attachment(htmlContent: "hello") 59 | let attachments = [htmlAttachment1, imgAttachment, jsonAttachment, htmlAttachment2] 60 | let mail = Mail(from: from, to: [to], subject: "HTML with related attachment, plus additional attachment", text: text, attachments: attachments) 61 | 62 | XCTAssert(htmlAttachment1.isAlternative) 63 | XCTAssert(!jsonAttachment.isAlternative) 64 | XCTAssertEqual(mail.attachments, [htmlAttachment1, imgAttachment, jsonAttachment]) 65 | XCTAssert(mail.alternative!.isAlternative) 66 | XCTAssertEqual(mail.alternative!, htmlAttachment2) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestTLSMode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2018 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | class TestTLSMode: XCTestCase { 21 | static var allTests = [ 22 | ("testNormal", testNormal), 23 | /*("testIgnoreTLS", testIgnoreTLS),*/ 24 | ("testRequireTLS", testRequireTLS), 25 | ("testRequireSTARTTLS", testRequireSTARTTLS) 26 | ] 27 | 28 | func testNormal() { 29 | let expectation = self.expectation(description: #function) 30 | defer { waitForExpectations(timeout: testDuration) } 31 | 32 | do { 33 | _ = try SMTPSocket( 34 | hostname: hostname, 35 | email: email, 36 | password: password, 37 | port: portPlain, 38 | tlsMode: .normal, 39 | tlsConfiguration: nil, 40 | authMethods: authMethods, 41 | domainName: domainName, 42 | timeout: timeout 43 | ) 44 | expectation.fulfill() 45 | } catch { 46 | XCTFail(String(describing: error)) 47 | expectation.fulfill() 48 | } 49 | } 50 | 51 | // This test is for a mail server that requires STARTTLS authentication. The current mail server being used for CI builds does not require STARTTLS for non-SSL ports. So this test cannot pass successfully. 52 | /* 53 | func testIgnoreTLS() { 54 | let expectation = self.expectation(description: #function) 55 | defer { waitForExpectations(timeout: testDuration) } 56 | 57 | do { 58 | _ = try SMTPSocket( 59 | hostname: hostname, 60 | email: email, 61 | password: password, 62 | port: portPlain, 63 | tlsMode: .ignoreTLS, 64 | tlsConfiguration: nil, 65 | authMethods: authMethods, 66 | domainName: domainName, 67 | timeout: timeout 68 | ) 69 | XCTFail() 70 | expectation.fulfill() 71 | } catch { 72 | if case SMTPError.noAuthMethodsOrRequiresTLS = error { 73 | expectation.fulfill() 74 | } else { 75 | XCTFail(String(describing: error)) 76 | expectation.fulfill() 77 | } 78 | } 79 | } 80 | */ 81 | 82 | func testRequireTLS() { 83 | let expectation = self.expectation(description: #function) 84 | defer { waitForExpectations(timeout: testDuration) } 85 | 86 | do { 87 | _ = try SMTPSocket( 88 | hostname: hostname, 89 | email: email, 90 | password: password, 91 | port: 465, 92 | tlsMode: .requireTLS, 93 | tlsConfiguration: nil, 94 | authMethods: authMethods, 95 | domainName: domainName, 96 | timeout: timeout 97 | ) 98 | expectation.fulfill() 99 | } catch { 100 | XCTFail(String(describing: error)) 101 | expectation.fulfill() 102 | } 103 | } 104 | 105 | func testRequireSTARTTLS() { 106 | let expectation = self.expectation(description: #function) 107 | defer { waitForExpectations(timeout: testDuration) } 108 | 109 | do { 110 | _ = try SMTPSocket( 111 | hostname: hostname, 112 | email: email, 113 | password: password, 114 | port: portPlain, 115 | tlsMode: .requireSTARTTLS, 116 | tlsConfiguration: nil, 117 | authMethods: authMethods, 118 | domainName: domainName, 119 | timeout: timeout 120 | ) 121 | expectation.fulfill() 122 | } catch { 123 | XCTFail(String(describing: error)) 124 | expectation.fulfill() 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/MailSender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | #if os(Linux) 20 | import Dispatch 21 | #endif 22 | 23 | /// (`Mail`, `Error`) callback after each `Mail` is sent. `Mail` is the mail sent and `Error` is the error if it failed. 24 | public typealias Progress = ((Mail, Error?) -> Void)? 25 | 26 | /// ([`Mail`], [(`Mail`, `Error`)]) callback after all `Mail`s have been attempted. [`Mail`] is an array of successfully 27 | /// sent `Mail`s. [(`Mail`, `Error`)] is an array of failed `Mail`s and their corresponding `Error`s. 28 | public typealias Completion = (([Mail], [(Mail, Error)]) -> Void)? 29 | 30 | class MailSender { 31 | private var socket: SMTPSocket 32 | private var mailsToSend: [Mail] 33 | private var progress: Progress 34 | private var completion: Completion 35 | private var sent = [Mail]() 36 | private var failed = [(Mail, Error)]() 37 | private var dataSender: DataSender 38 | 39 | init(socket: SMTPSocket, 40 | mailsToSend: [Mail], 41 | progress: Progress, 42 | completion: Completion) { 43 | self.socket = socket 44 | self.mailsToSend = mailsToSend 45 | self.progress = progress 46 | self.completion = completion 47 | dataSender = DataSender(socket: socket) 48 | } 49 | 50 | func send() { 51 | DispatchQueue.global().async { 52 | self.sendNext() 53 | } 54 | } 55 | } 56 | 57 | private extension MailSender { 58 | func sendNext() { 59 | if mailsToSend.isEmpty { 60 | completion?(sent, failed) 61 | progress = nil 62 | completion = nil 63 | try? quit() 64 | return 65 | } 66 | let mail = mailsToSend.removeFirst() 67 | do { 68 | try send(mail) 69 | if completion != nil { 70 | sent.append(mail) 71 | } 72 | progress?(mail, nil) 73 | } catch { 74 | if completion != nil { 75 | failed.append((mail, error)) 76 | } 77 | progress?(mail, error) 78 | } 79 | DispatchQueue.global().async { 80 | self.sendNext() 81 | } 82 | } 83 | 84 | func quit() throws { 85 | try socket.send(.quit) 86 | socket.close() 87 | } 88 | 89 | func send(_ mail: Mail) throws { 90 | let recipientEmails = try getRecipientEmails(from: mail) 91 | try validateEmails(recipientEmails) 92 | try sendMail(mail.from.email) 93 | try sendTo(recipientEmails) 94 | try data() 95 | try dataSender.send(mail) 96 | try dataEnd() 97 | } 98 | 99 | func getRecipientEmails(from mail: Mail) throws -> [String] { 100 | var recipientEmails = mail.to.map { $0.email } 101 | recipientEmails += mail.cc.map { $0.email } 102 | recipientEmails += mail.bcc.map { $0.email } 103 | 104 | guard !recipientEmails.isEmpty else { 105 | throw SMTPError.noRecipients 106 | } 107 | 108 | return recipientEmails 109 | } 110 | 111 | func validateEmails(_ emails: [String]) throws { 112 | for email in emails where try !email.isValidEmail() { 113 | throw SMTPError.invalidEmail(email: email) 114 | } 115 | } 116 | 117 | func sendMail(_ from: String) throws { 118 | try socket.send(.mail(from)) 119 | } 120 | 121 | func sendTo(_ emails: [String]) throws { 122 | for email in emails { 123 | try socket.send(.rcpt(email)) 124 | } 125 | } 126 | 127 | func data() throws { 128 | try socket.send(.data) 129 | } 130 | 131 | func dataEnd() throws { 132 | try socket.send(.dataEnd) 133 | } 134 | } 135 | 136 | private extension NSRegularExpression { 137 | static let emailRegex = try? NSRegularExpression(pattern: "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}") 138 | } 139 | 140 | private extension String { 141 | func isValidEmail() throws -> Bool { 142 | guard let emailRegex = NSRegularExpression.emailRegex else { 143 | throw SMTPError.createEmailRegexFailed 144 | } 145 | let range = NSRange(location: 0, length: count) 146 | return !emailRegex.matches(in: self, options: [], range: range).isEmpty 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestDataSender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | #if os(Linux) 21 | import Dispatch 22 | #endif 23 | 24 | class TestDataSender: XCTestCase { 25 | static var allTests = [ 26 | ("testSendData", testSendData), 27 | ("testSendFile", testSendFile), 28 | ("testSendHTML", testSendHTML), 29 | ("testSendHTMLAlternative", testSendHTMLAlternative), 30 | ("testSendMultipleAttachments", testSendMultipleAttachments), 31 | ("testSendNonASCII", testSendNonASCII), 32 | ("testSendRelatedAttachment", testSendRelatedAttachment) 33 | ] 34 | 35 | func testSendData() { 36 | let x = expectation(description: "Send mail with data attachment.") 37 | let dataAttachment = Attachment(data: data, mime: "application/json", name: "file.json") 38 | let mail = Mail(from: from, to: [to], subject: "Data attachment", text: text, attachments: [dataAttachment]) 39 | smtp.send(mail) { (err) in 40 | XCTAssertNil(err, String(describing: err)) 41 | x.fulfill() 42 | } 43 | waitForExpectations(timeout: testDuration) 44 | } 45 | 46 | func testSendFile() { 47 | let x = expectation(description: "Send mail with file attachment.") 48 | let fileAttachment = Attachment(filePath: imgFilePath) 49 | let mail = Mail(from: from, to: [to], subject: "File attachment", text: text, attachments: [fileAttachment]) 50 | smtp.send(mail) { (err) in 51 | XCTAssertNil(err, String(describing: err)) 52 | x.fulfill() 53 | } 54 | waitForExpectations(timeout: testDuration) 55 | } 56 | 57 | func testSendHTML() { 58 | let x = expectation(description: "Send mail with HTML attachment.") 59 | let htmlAttachment = Attachment(htmlContent: html, alternative: false) 60 | let mail = Mail(from: from, to: [to], subject: "HTML attachment", text: text, attachments: [htmlAttachment]) 61 | smtp.send(mail) { (err) in 62 | XCTAssertNil(err, String(describing: err)) 63 | x.fulfill() 64 | } 65 | waitForExpectations(timeout: testDuration) 66 | } 67 | 68 | func testSendHTMLAlternative() { 69 | let x = expectation(description: "Send mail with HTML as alternative to text.") 70 | let htmlAttachment = Attachment(htmlContent: html) 71 | let mail = Mail(from: from, to: [to], subject: "HTML alternative attachment", text: text, attachments: [htmlAttachment]) 72 | smtp.send(mail) { (err) in 73 | XCTAssertNil(err, String(describing: err)) 74 | x.fulfill() 75 | } 76 | waitForExpectations(timeout: testDuration) 77 | } 78 | 79 | func testSendMultipleAttachments() { 80 | let x = expectation(description: "Send mail with multiple attachments.") 81 | let fileAttachment = Attachment(filePath: imgFilePath) 82 | let htmlAttachment = Attachment(htmlContent: html, alternative: false) 83 | let mail = Mail(from: from, to: [to], subject: "Multiple attachments", text: text, attachments: [fileAttachment, htmlAttachment]) 84 | smtp.send(mail) { (err) in 85 | XCTAssertNil(err, String(describing: err)) 86 | x.fulfill() 87 | } 88 | waitForExpectations(timeout: testDuration) 89 | } 90 | 91 | func testSendNonASCII() { 92 | let x = expectation(description: "Send mail with non ASCII character.") 93 | let mail = Mail(from: from, to: [to], subject: "Non ASCII", text: "💦") 94 | smtp.send(mail) { (err) in 95 | XCTAssertNil(err, String(describing: err)) 96 | x.fulfill() 97 | } 98 | waitForExpectations(timeout: testDuration) 99 | } 100 | 101 | func testSendRelatedAttachment() { 102 | let x = expectation(description: "Send mail with an attachment that references a related attachment.") 103 | let fileAttachment = Attachment(filePath: imgFilePath, additionalHeaders: ["CONTENT-ID": "megaman-pic"]) 104 | let htmlAttachment = Attachment(htmlContent: "\(text)", relatedAttachments: [fileAttachment]) 105 | let mail = Mail(from: from, to: [to], subject: "HTML with related attachment", text: text, attachments: [htmlAttachment]) 106 | smtp.send(mail) { (err) in 107 | XCTAssertNil(err, String(describing: err)) 108 | x.fulfill() 109 | } 110 | waitForExpectations(timeout: testDuration) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestSMTPSocket.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | class TestSMTPSocket: XCTestCase { 21 | static var allTests = [ 22 | ("testBadCredentials", testBadCredentials), 23 | ("testBadPort", testBadPort), 24 | ("testLogin", testLogin), 25 | ("testPlain", testPlain), 26 | ("testPort0", testPort0), 27 | ("testSSL", testSSL) 28 | ] 29 | 30 | func testBadCredentials() throws { 31 | let x = expectation(description: #function) 32 | defer { waitForExpectations(timeout: testDuration) } 33 | 34 | do { 35 | _ = try SMTPSocket( 36 | hostname: hostname, 37 | email: email, 38 | password: "bad password", 39 | port: portPlain, 40 | tlsMode: .requireSTARTTLS, 41 | tlsConfiguration: nil, 42 | authMethods: authMethods, 43 | domainName: domainName, 44 | timeout: timeout 45 | ) 46 | XCTFail() 47 | x.fulfill() 48 | } catch { 49 | if case SMTPError.badResponse = error { 50 | x.fulfill() 51 | } else { 52 | XCTFail(String(describing: error)) 53 | x.fulfill() 54 | } 55 | } 56 | } 57 | 58 | func testBadPort() throws { 59 | let x = expectation(description: #function) 60 | defer { waitForExpectations(timeout: testDuration) } 61 | 62 | do { 63 | _ = try SMTPSocket( 64 | hostname: hostname, 65 | email: email, 66 | password: password, 67 | port: 1, 68 | tlsMode: .requireSTARTTLS, 69 | tlsConfiguration: nil, 70 | authMethods: authMethods, 71 | domainName: domainName, 72 | timeout: 5 73 | ) 74 | XCTFail() 75 | x.fulfill() 76 | } catch { 77 | x.fulfill() 78 | } 79 | } 80 | 81 | func testLogin() throws { 82 | let x = expectation(description: #function) 83 | defer { waitForExpectations(timeout: testDuration) } 84 | 85 | do { 86 | _ = try SMTPSocket( 87 | hostname: hostname, 88 | email: email, 89 | password: password, 90 | port: portPlain, 91 | tlsMode: .requireSTARTTLS, 92 | tlsConfiguration: nil, 93 | authMethods: [AuthMethod.login.rawValue: .login], 94 | domainName: domainName, 95 | timeout: timeout 96 | ) 97 | x.fulfill() 98 | } catch { 99 | XCTFail(String(describing: error)) 100 | x.fulfill() 101 | } 102 | } 103 | 104 | func testPlain() throws { 105 | let x = expectation(description: #function) 106 | defer { waitForExpectations(timeout: testDuration) } 107 | 108 | do { 109 | _ = try SMTPSocket( 110 | hostname: hostname, 111 | email: email, 112 | password: password, 113 | port: portPlain, 114 | tlsMode: .requireSTARTTLS, 115 | tlsConfiguration: nil, 116 | authMethods: [AuthMethod.plain.rawValue: .plain], 117 | domainName: domainName, 118 | timeout: timeout 119 | ) 120 | x.fulfill() 121 | } catch { 122 | XCTFail(String(describing: error)) 123 | x.fulfill() 124 | } 125 | } 126 | 127 | func testPort0() throws { 128 | let x = expectation(description: #function) 129 | defer { waitForExpectations(timeout: testDuration) } 130 | 131 | do { 132 | _ = try SMTPSocket( 133 | hostname: hostname, 134 | email: email, 135 | password: password, 136 | port: 0, 137 | tlsMode: .requireSTARTTLS, 138 | tlsConfiguration: nil, 139 | authMethods: authMethods, 140 | domainName: domainName, 141 | timeout: timeout 142 | ) 143 | XCTFail() 144 | x.fulfill() 145 | } catch { 146 | x.fulfill() 147 | } 148 | } 149 | 150 | func testSSL() throws { 151 | let x = self.expectation(description: #function) 152 | defer { waitForExpectations(timeout: testDuration) } 153 | 154 | do { 155 | _ = try SMTPSocket( 156 | hostname: hostname, 157 | email: email, 158 | password: password, 159 | port: portPlain, 160 | tlsMode: .requireSTARTTLS, 161 | tlsConfiguration: tlsConfiguration, 162 | authMethods: authMethods, 163 | domainName: domainName, 164 | timeout: timeout 165 | ) 166 | x.fulfill() 167 | } catch { 168 | XCTFail(String(describing: error)) 169 | x.fulfill() 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestMiscellaneous.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | #if os(Linux) && !swift(>=3.1) 21 | import Foundation 22 | #endif 23 | 24 | class TestMiscellaneous: XCTestCase { 25 | static var allTests = [ 26 | ("testBase64Encoded", testBase64Encoded), 27 | ("testMimeEncoded", testMimeEncoded), 28 | ("testDateFormatter", testDateFormatter), 29 | ("testMailHeaders", testMailHeaders), 30 | ("testMimeNoName", testMimeNoName), 31 | ("testMimeWithName", testMimeWithName) 32 | ] 33 | } 34 | 35 | // Common 36 | extension TestMiscellaneous { 37 | func testBase64Encoded() { 38 | let result1 = randomText1.base64Encoded 39 | XCTAssertEqual(result1, randomText1Encoded, "result: \(result1) != expected: \(randomText1Encoded)") 40 | 41 | let result2 = randomText2.base64Encoded 42 | XCTAssertEqual(result2, randomText2Encoded, "result: \(result2) != expected: \(randomText2Encoded)") 43 | 44 | let result3 = randomText3.base64Encoded 45 | XCTAssertEqual(result3, randomText3Encoded, "result: \(result3) != expected: \(randomText3Encoded)") 46 | } 47 | 48 | func testBase64EncodedWithLineLimit() { 49 | let result1 = randomText1.base64EncodedWithLineLimit 50 | XCTAssertEqual(result1, randomText1EncodedWithLineLimit, "result: \(result1) != expected: \(randomText1Encoded)") 51 | 52 | let result2 = randomText2.base64EncodedWithLineLimit 53 | XCTAssertEqual(result2, randomText2EncodedWithLineLimit, "result: \(result2) != expected: \(randomText2Encoded)") 54 | 55 | let result3 = randomText3.base64EncodedWithLineLimit 56 | XCTAssertEqual(result3, randomText3EncodedWithLineLimit, "result: \(result3) != expected: \(randomText3Encoded)") 57 | } 58 | 59 | 60 | func testMimeEncoded() { 61 | let result = "Water you up to?".mimeEncoded 62 | let expected = "=?UTF-8?Q?Water_you_up_to??=" 63 | XCTAssertEqual(result, expected, "result: \(String(describing: result)) != expected: \(expected)") 64 | } 65 | } 66 | 67 | // Mail 68 | extension TestMiscellaneous { 69 | func testDateFormatter() { 70 | let date = Date(timeIntervalSince1970: 0) 71 | let formatter = DateFormatter.smtpDateFormatter 72 | formatter.timeZone = TimeZone(secondsFromGMT: 3600 * 9) 73 | let result = formatter.string(from: date) 74 | let expected = "Thu, 1 Jan 1970 09:00:00 +0900" 75 | XCTAssertEqual(result, expected, "result: \(result) != expected: \(expected)") 76 | } 77 | 78 | func testMailHeaders() { 79 | let headers = Mail(from: from, to: [to], cc: [to2], subject: "Test", text: text, additionalHeaders: ["header": "val"]).headersString 80 | 81 | let to_ = "TO: =?UTF-8?Q?Megaman?= <\(email)>" 82 | XCTAssert(headers.contains(to_), "Mail header did not contain \(to_)") 83 | 84 | let cc_ = "CC: =?UTF-8?Q?Roll?= <\(email)>" 85 | XCTAssert(headers.contains(cc_), "Mail header did not contain \(cc_)") 86 | 87 | let subject = "SUBJECT: =?UTF-8?Q?Test?=" 88 | XCTAssert(headers.contains(subject), "Mail header did not contain \(subject)") 89 | 90 | let mimeVersion = "MIME-VERSION: 1.0 (Swift-SMTP)" 91 | XCTAssert(headers.contains(mimeVersion), "Mail header did not contain \(mimeVersion)") 92 | 93 | let messageIdSearchResponse = findMessageId(inString: headers) 94 | XCTAssert(validMessageIdMsg == messageIdSearchResponse, messageIdSearchResponse) 95 | 96 | XCTAssert(headers.contains("HEADER"), "Mail header did not contain \"header\".") 97 | XCTAssert(headers.contains("val"), "Mail header did not contain \"val\".") 98 | } 99 | 100 | private func findMessageId(inString compareString: String) -> String { 101 | let messageIdHeaderPrefix = "MESSAGE-ID: <" 102 | // example uuid: E621E1F8-C36C-495A-93FC-0C247A3E6E5F 103 | let uuidRegEx = "[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}" 104 | let messageIdHeaderSuffix = ".Swift-SMTP@\(senderEmailDomain)>" 105 | let regexPattern = "\(messageIdHeaderPrefix)\(uuidRegEx)\(messageIdHeaderSuffix)" 106 | 107 | guard let regex = try? NSRegularExpression(pattern: regexPattern, options: .anchorsMatchLines) else { 108 | return "Unable to create Regular Expression object" 109 | } 110 | 111 | let rangeLocation = 0 112 | let rangeLength = NSString(string: compareString).length 113 | let searchRange = NSRange(location: rangeLocation, length: rangeLength) 114 | 115 | // run the regex 116 | let matches = regex.matches(in: compareString, options: .withoutAnchoringBounds, range: searchRange) 117 | 118 | switch matches.count { 119 | case 0: 120 | return invalidMessageIdMsg 121 | case 1: 122 | return validMessageIdMsg 123 | default: 124 | return multipleMessageIdsMsg 125 | } 126 | } 127 | } 128 | 129 | // User 130 | extension TestMiscellaneous { 131 | func testMimeNoName() { 132 | let user = Mail.User(email: "bob@gmail.com") 133 | let expected = "bob@gmail.com" 134 | XCTAssertEqual(user.mime, expected, "result: \(user.mime) != expected: \(expected)") 135 | } 136 | 137 | func testMimeWithName() { 138 | let user = Mail.User(name: "Bob", email: "bob@gmail.com") 139 | let expected = "=?UTF-8?Q?Bob?= " 140 | XCTAssertEqual(user.mime, expected, "result: \(user.mime) != expected: \(expected)") 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/TLSConfiguration.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import SSLService 19 | 20 | /// Configuration for connecting with TLS. For more info, see https://github.com/Kitura/BlueSSLService. 21 | public struct TLSConfiguration { 22 | private let configuration: SSLService.Configuration 23 | 24 | /// 25 | /// Initialize a configuration with no backing certificates. 26 | /// 27 | /// - Parameters: 28 | /// - cipherSuite: Optional String containing the cipher suite to use. 29 | /// - clientAllowsSelfSignedCertificates: 30 | /// `true` to accept self-signed certificates from a server. `false` otherwise. 31 | public init(withCipherSuite cipherSuite: String? = nil, 32 | clientAllowsSelfSignedCertificates: Bool = false) { 33 | configuration = SSLService.Configuration( 34 | withCipherSuite: cipherSuite, 35 | clientAllowsSelfSignedCertificates: clientAllowsSelfSignedCertificates 36 | ) 37 | } 38 | 39 | /// 40 | /// Initialize a configuration using a `CA Certificate` file. 41 | /// 42 | /// - Parameters: 43 | /// - caCertificateFilePath: Path to the PEM formatted CA certificate file. 44 | /// - certificateFilePath: Path to the PEM formatted certificate file. 45 | /// - keyFilePath: Path to the PEM formatted key file. If nil, `certificateFilePath` will be used. 46 | /// - selfSigned: True if certs are `self-signed`, false otherwise. Defaults to true. 47 | /// - cipherSuite: Optional String containing the cipher suite to use. 48 | public init(withCACertificateFilePath caCertificateFilePath: String?, 49 | usingCertificateFile certificateFilePath: String?, 50 | withKeyFile keyFilePath: String? = nil, 51 | usingSelfSignedCerts selfSigned: Bool = true, 52 | cipherSuite: String? = nil) { 53 | configuration = SSLService.Configuration( 54 | withCACertificateFilePath: caCertificateFilePath, 55 | usingCertificateFile: certificateFilePath, 56 | withKeyFile: keyFilePath, 57 | usingSelfSignedCerts: selfSigned, 58 | cipherSuite: cipherSuite 59 | ) 60 | } 61 | 62 | /// 63 | /// Initialize a configuration using a `CA Certificate` directory. 64 | /// 65 | /// *Note:* `caCertificateDirPath` - All certificates in the specified directory **must** be hashed using the `OpenSSL Certificate Tool`. 66 | /// 67 | /// - Parameters: 68 | /// - caCertificateDirPath: Path to a directory containing CA certificates. *(see note above)* 69 | /// - certificateFilePath: Path to the PEM formatted certificate file. If nil, `certificateFilePath` will be used. 70 | /// - keyFilePath: Path to the PEM formatted key file (optional). If nil, `certificateFilePath` is used. 71 | /// - selfSigned: True if certs are `self-signed`, false otherwise. Defaults to true. 72 | /// - cipherSuite: Optional String containing the cipher suite to use. 73 | public init(withCACertificateDirectory caCertificateDirPath: String?, 74 | usingCertificateFile certificateFilePath: String?, 75 | withKeyFile keyFilePath: String? = nil, 76 | usingSelfSignedCerts selfSigned: Bool = true, 77 | cipherSuite: String? = nil) { 78 | configuration = SSLService.Configuration( 79 | withCACertificateDirectory: caCertificateDirPath, 80 | usingCertificateFile: certificateFilePath, 81 | withKeyFile: keyFilePath, 82 | usingSelfSignedCerts: selfSigned, 83 | cipherSuite: cipherSuite 84 | ) 85 | } 86 | 87 | /// 88 | /// Initialize a configuration using a `Certificate Chain File`. 89 | /// 90 | /// *Note:* If using a certificate chain file, the certificates must be in PEM format and must be sorted starting with the subject's certificate (actual client or server certificate), followed by intermediate CA certificates if applicable, and ending at the highest level (root) CA. 91 | /// 92 | /// - Parameters: 93 | /// - chainFilePath: Path to the certificate chain file (optional). *(see note above)* 94 | /// - password: Password for the chain file (optional). If using self-signed certs, a password is required. 95 | /// - selfSigned: True if certs are `self-signed`, false otherwise. Defaults to true. 96 | /// - clientAllowsSelfSignedCertificates: True if, as a client, connections to self-signed servers are allowed 97 | /// - cipherSuite: Optional String containing the cipher suite to use. 98 | public init(withChainFilePath chainFilePath: String?, 99 | withPassword password: String? = nil, 100 | usingSelfSignedCerts selfSigned: Bool = true, 101 | clientAllowsSelfSignedCertificates: Bool = false, 102 | cipherSuite: String? = nil) { 103 | configuration = SSLService.Configuration( 104 | withChainFilePath: chainFilePath, 105 | withPassword: password, 106 | usingSelfSignedCerts: selfSigned, 107 | clientAllowsSelfSignedCertificates: 108 | clientAllowsSelfSignedCertificates, 109 | cipherSuite: cipherSuite 110 | ) 111 | } 112 | 113 | #if os(Linux) 114 | /// 115 | /// Initialize a configuration using a `PEM formatted certificate in String form`. 116 | /// 117 | /// - Parameters: 118 | /// - certificateString: PEM formatted certificate in String form. 119 | /// - selfSigned: True if certs are `self-signed`, false otherwise. Defaults to true. 120 | /// - cipherSuite: Optional String containing the cipher suite to use. 121 | public init(withPEMCertificateString certificateString: String, 122 | usingSelfSignedCerts selfSigned: Bool = true, 123 | cipherSuite: String? = nil) { 124 | configuration = SSLService.Configuration( 125 | withPEMCertificateString: certificateString, 126 | usingSelfSignedCerts: selfSigned, 127 | cipherSuite: cipherSuite 128 | ) 129 | } 130 | #endif 131 | 132 | func makeSSLService() throws -> SSLService? { 133 | return try SSLService(usingConfiguration: configuration) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift-SMTP 2 | 3 | ![Swift-SMTP bird](https://github.com/Kitura/Swift-SMTP/blob/master/Assets/swift-smtp-bird.png?raw=true) 4 | 5 | Swift SMTP client. 6 | 7 | ![Build Status](https://travis-ci.org/Kitura/Swift-SMTP.svg?branch=master) 8 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) 9 | ![Linux](https://img.shields.io/badge/os-linux-green.svg?style=flat) 10 | ![Apache 2](https://img.shields.io/badge/license-Apache2-blue.svg?style=flat) 11 | 12 | ## Features 13 | 14 | - Connect securely through SSL/TLS when needed 15 | - Authenticate with CRAM-MD5, LOGIN, PLAIN, or XOAUTH2 16 | - Send emails with local file, HTML, and raw data attachments 17 | - Add custom headers 18 | - [Documentation](https://kitura.github.io/Swift-SMTP/) 19 | 20 | ## Swift Version 21 | 22 | macOS & Linux: `Swift 5.2` or above. 23 | 24 | ## Installation 25 | 26 | You can add `SwiftSMTP` to your project using [Swift Package Manager](https://swift.org/package-manager/). If your project does not have a `Package.swift` file, create one by running `swift package init` in the root directory of your project. Then open `Package.swift` and add `SwiftSMTP` as a dependency. Be sure to add it to your desired targets as well: 27 | 28 | ```swift 29 | // swift-tools-version:4.0 30 | 31 | import PackageDescription 32 | 33 | let package = Package( 34 | name: "MyProject", 35 | products: [ 36 | .library( 37 | name: "MyProject", 38 | targets: ["MyProject"]), 39 | ], 40 | dependencies: [ 41 | .package(url: "https://github.com/Kitura/Swift-SMTP", .upToNextMinor(from: "5.1.0")), // add the dependency 42 | ], 43 | targets: [ 44 | .target( 45 | name: "MyProject", 46 | dependencies: ["SwiftSMTP"]), // add targets 47 | .testTarget( // note "SwiftSMTP" (NO HYPHEN) 48 | name: "MyProjectTests", 49 | dependencies: ["MyProject"]), 50 | ] 51 | ) 52 | ``` 53 | 54 | After adding the dependency and saving, run `swift package generate-xcodeproj` in the root directory of your project. This will fetch dependencies and create an Xcode project which you can open and begin editing. 55 | 56 | ## Migration Guide 57 | 58 | Version `5.0.0` brings breaking changes. See the quick migration guide [here](https://github.com/Kitura/Swift-SMTP/blob/master/migration-guide.md). 59 | 60 | ## Usage 61 | 62 | Initialize an `SMTP` instance: 63 | 64 | ```swift 65 | import SwiftSMTP 66 | 67 | let smtp = SMTP( 68 | hostname: "smtp.gmail.com", // SMTP server address 69 | email: "user@gmail.com", // username to login 70 | password: "password" // password to login 71 | ) 72 | ``` 73 | 74 | ### TLS 75 | 76 | Additional parameters of `SMTP` struct: 77 | 78 | ```swift 79 | public init(hostname: String, 80 | email: String, 81 | password: String, 82 | port: Int32 = 587, 83 | tlsMode: TLSMode = .requireSTARTTLS, 84 | tlsConfiguration: TLSConfiguration? = nil, 85 | authMethods: [AuthMethod] = [], 86 | domainName: String = "localhost", 87 | timeout: UInt = 10) 88 | ``` 89 | 90 | By default, the `SMTP` struct connects on port `587` and sends mail only if a TLS connection can be established. It also uses a `TLSConfiguration` that uses no backing certificates. View the [docs](https://kitura.github.io/Swift-SMTP/) for more configuration options. 91 | 92 | ### Send email 93 | 94 | Create a `Mail` object and use your `SMTP` handle to send it. To set the sender and receiver of an email, use the `User` struct: 95 | 96 | ```swift 97 | let drLight = Mail.User(name: "Dr. Light", email: "drlight@gmail.com") 98 | let megaman = Mail.User(name: "Megaman", email: "megaman@gmail.com") 99 | 100 | let mail = Mail( 101 | from: drLight, 102 | to: [megaman], 103 | subject: "Humans and robots living together in harmony and equality.", 104 | text: "That was my ultimate wish." 105 | ) 106 | 107 | smtp.send(mail) { (error) in 108 | if let error = error { 109 | print(error) 110 | } 111 | } 112 | ``` 113 | 114 | Add Cc and Bcc: 115 | 116 | ```swift 117 | let roll = Mail.User(name: "Roll", email: "roll@gmail.com") 118 | let zero = Mail.User(name: "Zero", email: "zero@gmail.com") 119 | 120 | let mail = Mail( 121 | from: drLight, 122 | to: [megaman], 123 | cc: [roll], 124 | bcc: [zero], 125 | subject: "Robots should be used for the betterment of mankind.", 126 | text: "Any other use would be...unethical." 127 | ) 128 | 129 | smtp.send(mail) 130 | ``` 131 | 132 | ### Send attachments 133 | 134 | Create an `Attachment`, attach it to your `Mail`, and send it through the `SMTP` handle. Here's an example of how you can send the three supported types of attachments--a local file, HTML, and raw data: 135 | 136 | ```swift 137 | // Create a file `Attachment` 138 | let fileAttachment = Attachment( 139 | filePath: "~/img.png", 140 | // "CONTENT-ID" lets you reference this in another attachment 141 | additionalHeaders: ["CONTENT-ID": "img001"] 142 | ) 143 | 144 | // Create an HTML `Attachment` 145 | let htmlAttachment = Attachment( 146 | htmlContent: "Here's an image: ", 147 | // To reference `fileAttachment` 148 | related: [fileAttachment] 149 | ) 150 | 151 | // Create a data `Attachment` 152 | let data = "{\"key\": \"hello world\"}".data(using: .utf8)! 153 | let dataAttachment = Attachment( 154 | data: data, 155 | mime: "application/json", 156 | name: "file.json", 157 | // send as a standalone attachment 158 | inline: false 159 | ) 160 | 161 | // Create a `Mail` and include the `Attachment`s 162 | let mail = Mail( 163 | from: from, 164 | to: [to], 165 | subject: "Check out this image and JSON file!", 166 | // The attachments we created earlier 167 | attachments: [htmlAttachment, dataAttachment] 168 | ) 169 | 170 | // Send the mail 171 | smtp.send(mail) 172 | 173 | /* Each type of attachment has additional parameters for further customization */ 174 | ``` 175 | 176 | ### Send multiple mails 177 | 178 | ```swift 179 | let mail1: Mail = //... 180 | let mail2: Mail = //... 181 | 182 | smtp.send([mail1, mail2], 183 | // This optional callback gets called after each `Mail` is sent. 184 | // `mail` is the attempted `Mail`, `error` is the error if one occured. 185 | progress: { (mail, error) in 186 | }, 187 | 188 | // This optional callback gets called after all the mails have been sent. 189 | // `sent` is an array of the successfully sent `Mail`s. 190 | // `failed` is an array of (Mail, Error)--the failed `Mail`s and their corresponding errors. 191 | completion: { (sent, failed) in 192 | } 193 | ) 194 | ``` 195 | 196 | ## Acknowledgements 197 | 198 | Inspired by [Hedwig](https://github.com/onevcat/Hedwig) and [Perfect-SMTP](https://github.com/PerfectlySoft/Perfect-SMTP). 199 | 200 | ## License 201 | 202 | Apache v2.0 203 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/TestMailSender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import XCTest 18 | @testable import SwiftSMTP 19 | 20 | #if os(Linux) 21 | import Dispatch 22 | #endif 23 | 24 | class TestMailSender: XCTestCase { 25 | static var allTests = [ 26 | ("testBadEmail", testBadEmail), 27 | ("testSendMail", testSendMail), 28 | ("testSendMailInArray", testSendMailInArray), 29 | ("testSendMailNoRecipient", testSendMailNoRecipient), 30 | ("testSendMailsConcurrently", testSendMailsConcurrently), 31 | ("testSendMailToMultipleRecipients", testSendMailToMultipleRecipients), 32 | ("testSendMailWithBcc", testSendMailWithBcc), 33 | ("testSendMailWithCc", testSendMailWithCc), 34 | ("testSendMultipleMails", testSendMultipleMails), 35 | ("testSendMultipleMailsWithFail", testSendMultipleMailsWithFail), 36 | ("testSendNoMail", testSendNoMail) 37 | ] 38 | 39 | func testBadEmail() { 40 | let x = expectation(description: "Send a mail that will fail because of an invalid receiving address.") 41 | let user = Mail.User(email: "") 42 | let mail = Mail(from: user, to: [user]) 43 | smtp.send(mail) { (err) in 44 | XCTAssertNotNil(err, "Sending mail to an invalid email address should return an error, but return nil.") 45 | x.fulfill() 46 | } 47 | waitForExpectations(timeout: testDuration) 48 | } 49 | 50 | func testSendMail() { 51 | let x = expectation(description: #function) 52 | defer { waitForExpectations(timeout: testDuration) } 53 | 54 | let mail = Mail(from: from, to: [to], subject: #function, text: text) 55 | smtp.send(mail) { (err) in 56 | XCTAssertNil(err, String(describing: err)) 57 | x.fulfill() 58 | } 59 | } 60 | 61 | func testSendMailInArray() { 62 | let x = expectation(description: #function) 63 | defer { waitForExpectations(timeout: testDuration) } 64 | 65 | let mail = Mail(from: from, to: [to], subject: #function, text: text) 66 | smtp.send([mail], completion: { _, failed in 67 | XCTAssert(failed.isEmpty) 68 | x.fulfill() 69 | }) 70 | } 71 | 72 | func testSendMailNoRecipient() { 73 | let x = expectation(description: #function) 74 | defer { waitForExpectations(timeout: testDuration) } 75 | 76 | let mail = Mail(from: from, to: [], subject: #function, text: text) 77 | smtp.send(mail) { (error) in 78 | guard let error = error as? SMTPError, case .noRecipients = error else { 79 | XCTFail("Received different error other than SMTPError.noRecipients or no error at all.") 80 | return 81 | } 82 | x.fulfill() 83 | } 84 | } 85 | 86 | func testSendMailsConcurrently() { 87 | let x = expectation(description: "Send multiple mails concurrently with seperate calls to `send`.") 88 | let mail1 = Mail(from: from, to: [to], subject: "Send mails concurrently 1") 89 | let mail2 = Mail(from: from, to: [to], subject: "Send mails concurrently 2") 90 | let mails = [mail1, mail2] 91 | let group = DispatchGroup() 92 | for mail in mails { 93 | group.enter() 94 | smtp.send(mail) { (err) in 95 | XCTAssertNil(err, String(describing: err)) 96 | group.leave() 97 | } 98 | } 99 | group.wait() 100 | x.fulfill() 101 | waitForExpectations(timeout: testDuration) 102 | } 103 | 104 | func testSendMailToMultipleRecipients() { 105 | let x = expectation(description: "Send a single mail to multiple recipients.") 106 | let mail = Mail(from: from, to: [to, to2], subject: #function) 107 | smtp.send(mail) { (err) in 108 | XCTAssertNil(err, String(describing: err)) 109 | x.fulfill() 110 | } 111 | waitForExpectations(timeout: testDuration) 112 | } 113 | 114 | func testSendMailWithBcc() { 115 | let x = expectation(description: "Send mail with Bcc.") 116 | let mail = Mail(from: from, to: [to], bcc: [to2], subject: #function) 117 | smtp.send(mail) { (err) in 118 | XCTAssertNil(err, String(describing: err)) 119 | x.fulfill() 120 | } 121 | waitForExpectations(timeout: testDuration) 122 | } 123 | 124 | func testSendMailWithCc() { 125 | let x = expectation(description: "Send mail with Cc.") 126 | let mail = Mail(from: from, to: [to], cc: [to2], subject: #function) 127 | smtp.send(mail) { (err) in 128 | XCTAssertNil(err, String(describing: err)) 129 | x.fulfill() 130 | } 131 | waitForExpectations(timeout: testDuration) 132 | } 133 | 134 | func testSendMultipleMails() { 135 | let x = expectation(description: "Send multiple mails with one call to `send`.") 136 | let mail1 = Mail(from: from, to: [to], subject: "Send multiple mails 1") 137 | let mail2 = Mail(from: from, to: [to], subject: "Send multiple mails 2") 138 | smtp.send([mail1, mail2], progress: { (_, err) in 139 | XCTAssertNil(err, String(describing: err)) 140 | }) { (sent, failed) in 141 | XCTAssert(failed.isEmpty, "Some mails failed to send.") 142 | XCTAssertEqual(sent.count, 2, "2 mails should have been sent.") 143 | x.fulfill() 144 | } 145 | waitForExpectations(timeout: testDuration) 146 | } 147 | 148 | func testSendMultipleMailsWithFail() { 149 | let x = expectation(description: "Send two mails, one of which will fail.") 150 | let badUser = Mail.User(email: "") 151 | let badMail = Mail(from: from, to: [badUser]) 152 | let goodMail = Mail(from: from, to: [to], subject: "Send multiple mails with fail") 153 | smtp.send([badMail, goodMail], completion: { (sent, failed) in 154 | guard sent.count == 1 && failed.count == 1 else { 155 | XCTFail("Send did not complete with 1 mail sent and 1 mail failed.") 156 | return 157 | } 158 | XCTAssertEqual(sent[0].id, goodMail.id, "Valid email was not sent.") 159 | XCTAssertEqual(failed[0].0.id, badMail.id, "Invalid email returned does not match the invalid email sent.") 160 | XCTAssertNotNil(failed[0].1, "Invalid email did not return an error when sending.") 161 | x.fulfill() 162 | }) 163 | waitForExpectations(timeout: testDuration) 164 | } 165 | 166 | func testSendNoMail() { 167 | let x = expectation(description: #function) 168 | defer { waitForExpectations(timeout: testDuration) } 169 | smtp.send([], completion: { (sent, failed) in 170 | XCTAssert(sent.isEmpty) 171 | XCTAssert(failed.isEmpty) 172 | x.fulfill() 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/SMTP.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | /// Used to connect to an SMTP server and send emails. 20 | public struct SMTP { 21 | private let hostname: String 22 | private let email: String 23 | private let password: String 24 | private let port: Int32 25 | private let tlsMode: TLSMode 26 | private let tlsConfiguration: TLSConfiguration? 27 | private let authMethods: [String: AuthMethod] 28 | private let domainName: String 29 | private let timeout: UInt 30 | 31 | /// TLSMode enum for what form of connection security to enforce. 32 | public enum TLSMode { 33 | /// Upgrades the connection to TLS if STARTLS command is received, else sends mail without security. 34 | case normal 35 | 36 | /// Send mail over plaintext and ignore STARTTLS commands and TLS options. Could throw an error if server requires TLS. 37 | case ignoreTLS 38 | 39 | /// Only send mail after an initial successful TLS connection. Connection will fail if a TLS connection cannot be established. The default port, 587, will likely need to be adjusted depending on your server. 40 | case requireTLS 41 | 42 | /// Expect a STARTTLS command from the server and require the connection is upgraded to TLS. Will throw if the server does not issue a STARTTLS command. 43 | case requireSTARTTLS 44 | } 45 | 46 | /// Initializes an `SMTP` instance. 47 | /// 48 | /// - Parameters: 49 | /// - hostname: Hostname of the SMTP server to connect to, i.e. `smtp.example.com`. 50 | /// - email: Username to log in to server. 51 | /// - password: Password to log in to server, or access token if using XOAUTH2 authorization method. 52 | /// - port: Port to connect to the server on. Defaults to `465`. 53 | /// - tlsMode: TLSMode `enum` indicating what form of connection security to use. 54 | /// - tlsConfiguration: `TLSConfiguration` used to connect with TLS. If nil, a configuration with no backing 55 | /// certificates is used. See `TLSConfiguration` for other configuration options. 56 | /// - authMethods: `AuthMethod`s to use to log in to the server. If blank, tries all supported methods. 57 | /// - domainName: Client domain name used when communicating with the server. Defaults to `localhost`. 58 | /// - timeout: How long to try connecting to the server to before returning an error. Defaults to `10` seconds. 59 | /// 60 | /// - Note: 61 | /// - You may need to enable access for less secure apps for your account on the SMTP server. 62 | /// - Some servers like Gmail support IPv6, and if your network does not, you will first attempt to connect via 63 | /// IPv6, then timeout, and fall back to IPv4. You can avoid this by disabling IPv6 on your machine. 64 | public init(hostname: String, 65 | email: String, 66 | password: String, 67 | port: Int32 = 587, 68 | tlsMode: TLSMode = .requireSTARTTLS, 69 | tlsConfiguration: TLSConfiguration? = nil, 70 | authMethods: [AuthMethod] = [], 71 | domainName: String = "localhost", 72 | timeout: UInt = 10) { 73 | self.hostname = hostname 74 | self.email = email 75 | self.password = password 76 | self.port = port 77 | self.tlsMode = tlsMode 78 | self.tlsConfiguration = tlsConfiguration 79 | 80 | let _authMethods = !authMethods.isEmpty ? authMethods : [ 81 | AuthMethod.cramMD5, 82 | AuthMethod.login, 83 | AuthMethod.plain, 84 | AuthMethod.xoauth2 85 | ] 86 | var authMethodsDictionary = [String: AuthMethod]() 87 | _authMethods.forEach { authMethod in 88 | authMethodsDictionary[authMethod.rawValue] = authMethod 89 | } 90 | self.authMethods = authMethodsDictionary 91 | 92 | self.domainName = domainName 93 | self.timeout = timeout 94 | } 95 | 96 | /// Send an email. 97 | /// 98 | /// - Parameters: 99 | /// - mail: `Mail` object to send. 100 | /// - completion: Callback when sending finishes. `Error` is nil on success. (optional) 101 | public func send(_ mail: Mail, completion: ((Error?) -> Void)? = nil) { 102 | send([mail], completion: { (_, failed) in 103 | if let error = failed.first?.1 { 104 | completion?(error) 105 | } else { 106 | completion?(nil) 107 | } 108 | }) 109 | } 110 | 111 | /// Send multiple emails. 112 | /// 113 | /// - Parameters: 114 | /// - mails: Array of `Mail`s to send. 115 | /// - progress: (`Mail`, `Error`) callback after each `Mail` is sent. `Mail` is the mail sent and `Error` is 116 | /// the error if it failed. (optional) 117 | /// - completion: ([`Mail`], [(`Mail`, `Error`)]) callback after all `Mail`s have been attempted. [`Mail`] is an 118 | /// array of successfully sent `Mail`s. [(`Mail`, `Error`)] is an array of failed `Mail`s and their 119 | /// corresponding `Error`s. (optional) 120 | /// 121 | /// - Note: 122 | /// - Each call to `send` will first log in to your server, attempt to send the mails, then closes the 123 | /// connection. Pass in an array of `Mail`s to send them all in one session. 124 | /// - If any of the email addresses in a `Mail`'s `to`, `cc`, or `bcc` are invalid, the entire mail will not 125 | /// send and return an `SMTPError`. 126 | /// - If an individual `Mail` fails while sending an array of `Mail`s, the whole sending process will continue 127 | /// until all pending `Mail`s are attempted. 128 | /// - Each call to `send` queues it's `Mail`s and sends them one by one. To send `Mail`s concurrently, send them 129 | /// in separate calls to `send`. 130 | public func send(_ mails: [Mail], 131 | progress: Progress = nil, 132 | completion: Completion = nil) { 133 | if mails.isEmpty { 134 | completion?([], []) 135 | return 136 | } 137 | do { 138 | let socket = try SMTPSocket( 139 | hostname: hostname, 140 | email: email, 141 | password: password, 142 | port: port, 143 | tlsMode: tlsMode, 144 | tlsConfiguration: tlsConfiguration, 145 | authMethods: authMethods, 146 | domainName: domainName, 147 | timeout: timeout 148 | ) 149 | MailSender( 150 | socket: socket, 151 | mailsToSend: mails, 152 | progress: progress, 153 | completion: completion).send() 154 | } catch { 155 | completion?([], mails.map { ($0, error) }) 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/Mail.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | /// Represents an email that can be sent through an `SMTP` instance. 20 | public struct Mail { 21 | /// A UUID for the mail. 22 | public let uuid = UUID().uuidString 23 | 24 | /// The `User` that the `Mail` will be sent from. 25 | public let from: User 26 | 27 | /// Array of `User`s to send the `Mail` to. 28 | public let to: [User] 29 | 30 | /// Array of `User`s to cc. Defaults to none. 31 | public let cc: [User] 32 | 33 | /// Array of `User`s to bcc. Defaults to none. 34 | public let bcc: [User] 35 | 36 | /// Subject of the `Mail`. Defaults to none. 37 | public let subject: String 38 | 39 | /// Text of the `Mail`. Defaults to none. 40 | public let text: String 41 | 42 | /// Array of `Attachment`s for the `Mail`. If the `Mail` has multiple `Attachment`s that are alternatives to plain 43 | /// text, the last one will be used as the alternative (all the `Attachments` will still be sent). Defaults to none. 44 | public let attachments: [Attachment] 45 | 46 | /// Attachment that is an alternative to plain text. 47 | public let alternative: Attachment? 48 | 49 | /// Additional headers for the `Mail`. Header keys are capitalized and duplicate keys will overwrite each other. 50 | /// Defaults to none. The following will be ignored: CONTENT-TYPE, CONTENT-DISPOSITION, CONTENT-TRANSFER-ENCODING. 51 | public let additionalHeaders: [String: String] 52 | 53 | /// message-id https://tools.ietf.org/html/rfc5322#section-3.6.4 54 | public var id: String { 55 | return "<\(uuid).Swift-SMTP@\(hostname)>" 56 | } 57 | 58 | /// Hostname from the email address. 59 | public var hostname: String { 60 | let fullEmail = from.email 61 | #if swift(>=4.2) 62 | let atIndex = fullEmail.firstIndex(of: "@") 63 | #else 64 | let atIndex = fullEmail.index(of: "@") 65 | #endif 66 | let hostStart = fullEmail.index(after: atIndex!) 67 | return String(fullEmail[hostStart...]) 68 | } 69 | 70 | /// Initializes a `Mail` object. 71 | /// 72 | /// - Parameters: 73 | /// - from: The `User` that the `Mail` will be sent from. 74 | /// - to: Array of `User`s to send the `Mail` to. 75 | /// - cc: Array of `User`s to cc. Defaults to none. 76 | /// - bcc: Array of `User`s to bcc. Defaults to none. 77 | /// - subject: Subject of the `Mail`. Defaults to none. 78 | /// - text: Text of the `Mail`. Defaults to none. 79 | /// - attachments: Array of `Attachment`s for the `Mail`. If the `Mail` has multiple `Attachment`s that are 80 | /// alternatives to plain text, the last one will be used as the alternative (all the `Attachments` will still 81 | /// be sent). Defaults to none. 82 | /// - additionalHeaders: Additional headers for the `Mail`. Header keys are capitalized and duplicate keys will 83 | /// overwrite each other. Defaults to none. The following will be ignored: CONTENT-TYPE, CONTENT-DISPOSITION, 84 | /// CONTENT-TRANSFER-ENCODING. 85 | public init(from: User, 86 | to: [User], 87 | cc: [User] = [], 88 | bcc: [User] = [], 89 | subject: String = "", 90 | text: String = "", 91 | attachments: [Attachment] = [], 92 | additionalHeaders: [String: String] = [:]) { 93 | self.from = from 94 | self.to = to 95 | self.cc = cc 96 | self.bcc = bcc 97 | self.subject = subject 98 | self.text = text 99 | 100 | let (alternative, attachments) = Mail.getAlternative(attachments) 101 | self.alternative = alternative 102 | self.attachments = attachments 103 | 104 | self.additionalHeaders = additionalHeaders 105 | } 106 | 107 | private static func getAlternative(_ attachments: [Attachment]) -> (Attachment?, [Attachment]) { 108 | var reversed: [Attachment] = attachments.reversed() 109 | #if swift(>=4.2) 110 | let index = reversed.firstIndex(where: { $0.isAlternative }) 111 | #else 112 | let index = reversed.index(where: { $0.isAlternative }) 113 | #endif 114 | if let index = index { 115 | return (reversed.remove(at: index), reversed.reversed()) 116 | } 117 | return (nil, attachments) 118 | } 119 | 120 | private var headersDictionary: [String: String] { 121 | var dictionary = [String: String]() 122 | dictionary["MESSAGE-ID"] = id 123 | dictionary["DATE"] = Date().smtpFormatted 124 | dictionary["FROM"] = from.mime 125 | dictionary["TO"] = to.map { $0.mime }.joined(separator: ", ") 126 | 127 | if !cc.isEmpty { 128 | dictionary["CC"] = cc.map { $0.mime }.joined(separator: ", ") 129 | } 130 | 131 | dictionary["SUBJECT"] = subject.mimeEncoded ?? "" 132 | dictionary["MIME-VERSION"] = "1.0 (Swift-SMTP)" 133 | 134 | for (key, value) in additionalHeaders { 135 | let keyUppercased = key.uppercased() 136 | if keyUppercased != "CONTENT-TYPE" && 137 | keyUppercased != "CONTENT-DISPOSITION" && 138 | keyUppercased != "CONTENT-TRANSFER-ENCODING" { 139 | dictionary[keyUppercased] = value 140 | } 141 | } 142 | 143 | return dictionary 144 | } 145 | 146 | var headersString: String { 147 | return headersDictionary.map { (key, value) in 148 | return "\(key): \(value)" 149 | }.joined(separator: CRLF) 150 | } 151 | 152 | var hasAttachment: Bool { 153 | return !attachments.isEmpty || alternative != nil 154 | } 155 | } 156 | 157 | extension Mail { 158 | /// Represents a sender or receiver of an email. 159 | public struct User { 160 | /// The user's name that is displayed in an email. Optional. 161 | public let name: String? 162 | 163 | /// The user's email address. 164 | public let email: String 165 | 166 | /// Initializes a `User`. 167 | /// 168 | /// - Parameters: 169 | /// - name: The user's name that is displayed in an email. Optional. 170 | /// - email: The user's email address. 171 | public init(name: String? = nil, email: String) { 172 | self.name = name 173 | self.email = email 174 | } 175 | 176 | var mime: String { 177 | if let name = name, let nameEncoded = name.mimeEncoded { 178 | return "\(nameEncoded) <\(email)>" 179 | } else { 180 | return email 181 | } 182 | } 183 | } 184 | } 185 | 186 | extension DateFormatter { 187 | static let smtpDateFormatter: DateFormatter = { 188 | let formatter = DateFormatter() 189 | formatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss ZZZ" 190 | return formatter 191 | }() 192 | } 193 | 194 | extension Date { 195 | var smtpFormatted: String { 196 | return DateFormatter.smtpDateFormatter.string(from: self) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /docs/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | SwiftSMTP Docs 25 | 26 | (100% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 100 |
101 | 102 |
103 |
104 |

Enumerations

105 |

The following enumerations are available globally.

106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 |
    114 |
  • 115 |
    116 | 117 | 118 | 119 | AuthMethod 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 |

    Supported authentication methods for logging into the SMTP server.

    128 | 129 | See more 130 |
    131 |
    132 |

    Declaration

    133 |
    134 |

    Swift

    135 |
    public enum AuthMethod : String
    136 | 137 |
    138 |
    139 |
    140 |
    141 |
  • 142 |
  • 143 |
    144 | 145 | 146 | 147 | SMTPError 148 | 149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 |

    Error type for SwiftSMTP.

    156 | 157 | See more 158 |
    159 |
    160 |

    Declaration

    161 |
    162 |

    Swift

    163 |
    public enum SMTPError : Error, CustomStringConvertible
    164 | 165 |
    166 |
    167 |
    168 |
    169 |
  • 170 |
171 |
172 |
173 |
174 | 175 |
176 |
177 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | SwiftSMTP Docs 25 | 26 | (100% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 100 |
101 | 102 |
103 |
104 |

Enumerations

105 |

The following enumerations are available globally.

106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 |
    114 |
  • 115 |
    116 | 117 | 118 | 119 | AuthMethod 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 |

    Supported authentication methods for logging into the SMTP server.

    128 | 129 | See more 130 |
    131 |
    132 |

    Declaration

    133 |
    134 |

    Swift

    135 |
    public enum AuthMethod : String
    136 | 137 |
    138 |
    139 |
    140 |
    141 |
  • 142 |
  • 143 |
    144 | 145 | 146 | 147 | SMTPError 148 | 149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 |

    Error type for SwiftSMTP.

    156 | 157 | See more 158 |
    159 |
    160 |

    Declaration

    161 |
    162 |

    Swift

    163 |
    public enum SMTPError : Error, CustomStringConvertible
    164 | 165 |
    166 |
    167 |
    168 |
    169 |
  • 170 |
171 |
172 |
173 |
174 | 175 |
176 |
177 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /Tests/SwiftSMTPTests/Constant.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import SwiftSMTP 18 | import Foundation 19 | 20 | #if os(Linux) 21 | import Glibc 22 | #endif 23 | 24 | let testDuration: Double = 15 25 | 26 | // 📧📧📧 Fill in your own SMTP login info for local testing 27 | // ⚠️⚠️⚠️ DO NOT CHECK IN YOUR EMAIL CREDENTALS!!! 28 | let hostname = "mail.kitura.dev" 29 | let myEmail: String? = nil 30 | let myPassword: String? = nil 31 | let portTLS: Int32 = 465 32 | let portPlain: Int32 = 2525 33 | let authMethods: [String: AuthMethod] = [ 34 | AuthMethod.cramMD5.rawValue: .cramMD5, 35 | AuthMethod.login.rawValue: .login, 36 | AuthMethod.plain.rawValue: .plain 37 | ] 38 | let domainName = "localhost" 39 | let timeout: UInt = 10 40 | 41 | let email: String = { 42 | if let email = myEmail { 43 | return email 44 | } 45 | guard let email = ProcessInfo.processInfo.environment["EMAIL"] else { 46 | fatalError("Please provide email credentials for local testing.") 47 | } 48 | return email 49 | }() 50 | 51 | let password: String = { 52 | if let password = myPassword { 53 | return password 54 | } 55 | guard let password = ProcessInfo.processInfo.environment["PASSWORD"] else { 56 | fatalError("Please provide email credentials for local testing.") 57 | } 58 | return password 59 | }() 60 | 61 | let senderEmailDomain: String = { 62 | #if swift(>=5) 63 | if let atIndex = email.firstIndex(of: "@") { 64 | let domainStart = email.index(after: atIndex) 65 | return String(email[domainStart...]) 66 | } else { 67 | return "gmail.com" 68 | } 69 | #else 70 | if let atIndex = email.index(of: "@") { 71 | let domainStart = email.index(after: atIndex) 72 | return String(email[domainStart...]) 73 | } else { 74 | return "gmail.com" 75 | } 76 | #endif 77 | }() 78 | 79 | let testsDir: String = { 80 | return URL(fileURLWithPath: #file).appendingPathComponent("..").standardized.path 81 | }() 82 | 83 | #if os(Linux) 84 | let cert = testsDir + "/cert.pem" 85 | let key = testsDir + "/key.pem" 86 | let tlsConfiguration = TLSConfiguration(withCACertificateDirectory: nil, usingCertificateFile: cert, withKeyFile: key) 87 | #else 88 | let cert = testsDir + "/cert.pfx" 89 | let certPassword = "kitura" 90 | let tlsConfiguration = TLSConfiguration(withChainFilePath: cert, withPassword: certPassword) 91 | #endif 92 | 93 | let smtp = SMTP(hostname: hostname, email: email, password: password) 94 | let from = Mail.User(name: "Dr. Light", email: email) 95 | let to = Mail.User(name: "Megaman", email: email) 96 | let to2 = Mail.User(name: "Roll", email: email) 97 | let text = "Humans and robots living together in harmony and equality: That was my ultimate wish." 98 | let html = "" 99 | let imgFilePath = testsDir + "/x.png" 100 | let data = "{\"key\": \"hello world\"}".data(using: .utf8)! 101 | 102 | let validMessageIdMsg = "Valid Message-Id header found" 103 | let invalidMessageIdMsg = "Message-Id header missing or invalid" 104 | let multipleMessageIdsMsg = "More than one Message-Id header found" 105 | 106 | // https://www.base64decode.org/ 107 | let randomText1 = "Picture removal detract earnest is by. Esteems met joy attempt way clothes yet demesne tedious. Replying an marianne do it an entrance advanced. Two dare say play when hold. Required bringing me material stanhill jointure is as he. Mutual indeed yet her living result matter him bed whence." 108 | 109 | let randomText1Encoded = "UGljdHVyZSByZW1vdmFsIGRldHJhY3QgZWFybmVzdCBpcyBieS4gRXN0ZWVtcyBtZXQgam95IGF0dGVtcHQgd2F5IGNsb3RoZXMgeWV0IGRlbWVzbmUgdGVkaW91cy4gUmVwbHlpbmcgYW4gbWFyaWFubmUgZG8gaXQgYW4gZW50cmFuY2UgYWR2YW5jZWQuIFR3byBkYXJlIHNheSBwbGF5IHdoZW4gaG9sZC4gUmVxdWlyZWQgYnJpbmdpbmcgbWUgbWF0ZXJpYWwgc3RhbmhpbGwgam9pbnR1cmUgaXMgYXMgaGUuIE11dHVhbCBpbmRlZWQgeWV0IGhlciBsaXZpbmcgcmVzdWx0IG1hdHRlciBoaW0gYmVkIHdoZW5jZS4=" 110 | let randomText1EncodedWithLineLimit = """ 111 | UGljdHVyZSByZW1vdmFsIGRldHJhY3QgZWFybmVzdCBpcyBieS4gRXN0ZWVtcyBtZXQgam95IGF0 112 | dGVtcHQgd2F5IGNsb3RoZXMgeWV0IGRlbWVzbmUgdGVkaW91cy4gUmVwbHlpbmcgYW4gbWFyaWFu 113 | bmUgZG8gaXQgYW4gZW50cmFuY2UgYWR2YW5jZWQuIFR3byBkYXJlIHNheSBwbGF5IHdoZW4gaG9s 114 | ZC4gUmVxdWlyZWQgYnJpbmdpbmcgbWUgbWF0ZXJpYWwgc3RhbmhpbGwgam9pbnR1cmUgaXMgYXMg 115 | aGUuIE11dHVhbCBpbmRlZWQgeWV0IGhlciBsaXZpbmcgcmVzdWx0IG1hdHRlciBoaW0gYmVkIHdo 116 | ZW5jZS4= 117 | """.replacingOccurrences(of: "\n", with: "\r\n") 118 | 119 | let randomText2 = "Brillo viento gas esa contar hay. Alla no toda lune faro daba en pero. Ir rumiar altura id venian. El robusto hablado ya diarios tu hacerla mermado. Las sus renunciaba llamaradas misteriosa doscientas favorcillo dos pie. Una era fue pedirselos periodicos doscientas actualidad con. Exigian un en oh algunos adivino parezca notario yo. Eres oro dos mal lune vivo sepa les seda. Tio energia una esa abultar por tufillo sirenas persona suspiro. Me pandero tardaba pedirme puertas so senales la." 120 | 121 | let randomText2Encoded = "QnJpbGxvIHZpZW50byBnYXMgZXNhIGNvbnRhciBoYXkuIEFsbGEgbm8gdG9kYSBsdW5lIGZhcm8gZGFiYSBlbiBwZXJvLiBJciBydW1pYXIgYWx0dXJhIGlkIHZlbmlhbi4gRWwgcm9idXN0byBoYWJsYWRvIHlhIGRpYXJpb3MgdHUgaGFjZXJsYSBtZXJtYWRvLiBMYXMgc3VzIHJlbnVuY2lhYmEgbGxhbWFyYWRhcyBtaXN0ZXJpb3NhIGRvc2NpZW50YXMgZmF2b3JjaWxsbyBkb3MgcGllLiBVbmEgZXJhIGZ1ZSBwZWRpcnNlbG9zIHBlcmlvZGljb3MgZG9zY2llbnRhcyBhY3R1YWxpZGFkIGNvbi4gRXhpZ2lhbiB1biBlbiBvaCBhbGd1bm9zIGFkaXZpbm8gcGFyZXpjYSBub3RhcmlvIHlvLiBFcmVzIG9ybyBkb3MgbWFsIGx1bmUgdml2byBzZXBhIGxlcyBzZWRhLiBUaW8gZW5lcmdpYSB1bmEgZXNhIGFidWx0YXIgcG9yIHR1ZmlsbG8gc2lyZW5hcyBwZXJzb25hIHN1c3Bpcm8uIE1lIHBhbmRlcm8gdGFyZGFiYSBwZWRpcm1lIHB1ZXJ0YXMgc28gc2VuYWxlcyBsYS4=" 122 | let randomText2EncodedWithLineLimit = """ 123 | QnJpbGxvIHZpZW50byBnYXMgZXNhIGNvbnRhciBoYXkuIEFsbGEgbm8gdG9kYSBsdW5lIGZhcm8g 124 | ZGFiYSBlbiBwZXJvLiBJciBydW1pYXIgYWx0dXJhIGlkIHZlbmlhbi4gRWwgcm9idXN0byBoYWJs 125 | YWRvIHlhIGRpYXJpb3MgdHUgaGFjZXJsYSBtZXJtYWRvLiBMYXMgc3VzIHJlbnVuY2lhYmEgbGxh 126 | bWFyYWRhcyBtaXN0ZXJpb3NhIGRvc2NpZW50YXMgZmF2b3JjaWxsbyBkb3MgcGllLiBVbmEgZXJh 127 | IGZ1ZSBwZWRpcnNlbG9zIHBlcmlvZGljb3MgZG9zY2llbnRhcyBhY3R1YWxpZGFkIGNvbi4gRXhp 128 | Z2lhbiB1biBlbiBvaCBhbGd1bm9zIGFkaXZpbm8gcGFyZXpjYSBub3RhcmlvIHlvLiBFcmVzIG9y 129 | byBkb3MgbWFsIGx1bmUgdml2byBzZXBhIGxlcyBzZWRhLiBUaW8gZW5lcmdpYSB1bmEgZXNhIGFi 130 | dWx0YXIgcG9yIHR1ZmlsbG8gc2lyZW5hcyBwZXJzb25hIHN1c3Bpcm8uIE1lIHBhbmRlcm8gdGFy 131 | ZGFiYSBwZWRpcm1lIHB1ZXJ0YXMgc28gc2VuYWxlcyBsYS4= 132 | """.replacingOccurrences(of: "\n", with: "\r\n") 133 | 134 | let randomText3 = "Intueor veritas suo majoris attinet rem res aggredi similia mei. Disputari abducerem ob ex ha interitum conflatos concipiam. Curam plura aequo rem etc serio fecto caput. Ea posterum lectorem remanere experiar videamus gi cognitum vi. Ad invenit accepit to petitis ea usitata ad. Hoc nam quibus hos oculis cumque videam ita. Res cau infinitum quadratam sanguinem." 135 | 136 | let randomText3Encoded = "SW50dWVvciB2ZXJpdGFzIHN1byBtYWpvcmlzIGF0dGluZXQgcmVtIHJlcyBhZ2dyZWRpIHNpbWlsaWEgbWVpLiBEaXNwdXRhcmkgYWJkdWNlcmVtIG9iIGV4IGhhIGludGVyaXR1bSBjb25mbGF0b3MgY29uY2lwaWFtLiBDdXJhbSBwbHVyYSBhZXF1byByZW0gZXRjIHNlcmlvIGZlY3RvIGNhcHV0LiBFYSBwb3N0ZXJ1bSBsZWN0b3JlbSByZW1hbmVyZSBleHBlcmlhciB2aWRlYW11cyBnaSBjb2duaXR1bSB2aS4gQWQgaW52ZW5pdCBhY2NlcGl0IHRvIHBldGl0aXMgZWEgdXNpdGF0YSBhZC4gSG9jIG5hbSBxdWlidXMgaG9zIG9jdWxpcyBjdW1xdWUgdmlkZWFtIGl0YS4gUmVzIGNhdSBpbmZpbml0dW0gcXVhZHJhdGFtIHNhbmd1aW5lbS4=" 137 | let randomText3EncodedWithLineLimit = """ 138 | SW50dWVvciB2ZXJpdGFzIHN1byBtYWpvcmlzIGF0dGluZXQgcmVtIHJlcyBhZ2dyZWRpIHNpbWls 139 | aWEgbWVpLiBEaXNwdXRhcmkgYWJkdWNlcmVtIG9iIGV4IGhhIGludGVyaXR1bSBjb25mbGF0b3Mg 140 | Y29uY2lwaWFtLiBDdXJhbSBwbHVyYSBhZXF1byByZW0gZXRjIHNlcmlvIGZlY3RvIGNhcHV0LiBF 141 | YSBwb3N0ZXJ1bSBsZWN0b3JlbSByZW1hbmVyZSBleHBlcmlhciB2aWRlYW11cyBnaSBjb2duaXR1 142 | bSB2aS4gQWQgaW52ZW5pdCBhY2NlcGl0IHRvIHBldGl0aXMgZWEgdXNpdGF0YSBhZC4gSG9jIG5h 143 | bSBxdWlidXMgaG9zIG9jdWxpcyBjdW1xdWUgdmlkZWFtIGl0YS4gUmVzIGNhdSBpbmZpbml0dW0g 144 | cXVhZHJhdGFtIHNhbmd1aW5lbS4= 145 | """.replacingOccurrences(of: "\n", with: "\r\n") 146 | -------------------------------------------------------------------------------- /docs/Typealiases.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Type Aliases Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | SwiftSMTP Docs 25 | 26 | (100% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 100 |
101 | 102 |
103 |
104 |

Type Aliases

105 |

The following type aliases are available globally.

106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 |
    114 |
  • 115 |
    116 | 117 | 118 | 119 | Progress 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 |

    (Mail, Error) callback after each Mail is sent. Mail is the mail sent and Error is the error if it failed.

    128 | 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public typealias Progress = ((Mail, Error?) -> Void)?
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
  • 142 |
    143 | 144 | 145 | 146 | Completion 147 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |

    ([Mail], [(Mail, Error)]) callback after all Mails have been attempted. [Mail] is an array of successfully 155 | sent Mails. [(Mail, Error)] is an array of failed Mails and their corresponding Errors.

    156 | 157 |
    158 |
    159 |

    Declaration

    160 |
    161 |

    Swift

    162 |
    public typealias Completion = (([Mail], [(Mail, Error)]) -> Void)?
    163 | 164 |
    165 |
    166 |
    167 |
    168 |
  • 169 |
170 |
171 |
172 |
173 | 174 |
175 |
176 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/Typealiases.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Type Aliases Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | SwiftSMTP Docs 25 | 26 | (100% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 100 |
101 | 102 |
103 |
104 |

Type Aliases

105 |

The following type aliases are available globally.

106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 |
    114 |
  • 115 |
    116 | 117 | 118 | 119 | Progress 120 | 121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 |

    (Mail, Error) callback after each Mail is sent. Mail is the mail sent and Error is the error if it failed.

    128 | 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public typealias Progress = ((Mail, Error?) -> Void)?
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
  • 142 |
    143 | 144 | 145 | 146 | Completion 147 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |

    ([Mail], [(Mail, Error)]) callback after all Mails have been attempted. [Mail] is an array of successfully 155 | sent Mails. [(Mail, Error)] is an array of failed Mails and their corresponding Errors.

    156 | 157 |
    158 |
    159 |

    Declaration

    160 |
    161 |

    Swift

    162 |
    public typealias Completion = (([Mail], [(Mail, Error)]) -> Void)?
    163 | 164 |
    165 |
    166 |
    167 |
    168 |
  • 169 |
170 |
171 |
172 |
173 | 174 |
175 |
176 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/SMTPSocket.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2018 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import Socket 19 | import LoggerAPI 20 | 21 | struct SMTPSocket { 22 | private let socket: Socket 23 | 24 | init(hostname: String, 25 | email: String, 26 | password: String, 27 | port: Int32, 28 | tlsMode: SMTP.TLSMode, 29 | tlsConfiguration: TLSConfiguration?, 30 | authMethods: [String: AuthMethod], 31 | domainName: String, 32 | timeout: UInt) throws { 33 | socket = try Socket.create() 34 | if tlsMode == .requireTLS { 35 | if let tlsConfiguration = tlsConfiguration { 36 | socket.delegate = try tlsConfiguration.makeSSLService() 37 | } else { 38 | socket.delegate = try TLSConfiguration().makeSSLService() 39 | } 40 | } 41 | try socket.connect(to: hostname, port: port, timeout: timeout * 1000) 42 | try parseResponses(readFromSocket(), command: .connect) 43 | var serverOptions = try getServerOptions(domainName: domainName) 44 | if tlsMode == .requireSTARTTLS || tlsMode == .normal { 45 | if try doStarttls(serverOptions: serverOptions, tlsConfiguration: tlsConfiguration) { 46 | serverOptions = try getServerOptions(domainName: domainName) 47 | } else if tlsMode == .requireSTARTTLS { 48 | throw SMTPError.requiredSTARTTLS 49 | } 50 | } 51 | let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname) 52 | try login(authMethod: authMethod, email: email, password: password) 53 | } 54 | 55 | func write(_ text: String) throws { 56 | _ = try socket.write(from: text + CRLF) 57 | Log.debug(text) 58 | } 59 | 60 | func write(_ data: Data) throws { 61 | _ = try socket.write(from: data) 62 | Log.debug("(sending data)") 63 | } 64 | 65 | @discardableResult 66 | func send(_ command: Command) throws -> [Response] { 67 | try write(command.text) 68 | return try parseResponses(readFromSocket(), command: command) 69 | } 70 | 71 | func close() { 72 | socket.close() 73 | } 74 | } 75 | 76 | private extension SMTPSocket { 77 | func readFromSocket() throws -> String { 78 | var buf = Data() 79 | _ = try socket.read(into: &buf) 80 | guard let responses = String(data: buf, encoding: .utf8) else { 81 | throw SMTPError.convertDataUTF8Fail(data: buf) 82 | } 83 | Log.debug(responses) 84 | return responses 85 | } 86 | 87 | @discardableResult 88 | func parseResponses(_ responses: String, command: Command) throws -> [Response] { 89 | let responsesArray = responses.components(separatedBy: CRLF) 90 | guard !responsesArray.isEmpty else { 91 | throw SMTPError.badResponse(command: command.text, response: responses) 92 | } 93 | #if swift(>=4.1) 94 | return try responsesArray.compactMap { response in 95 | guard response != "" else { 96 | return nil 97 | } 98 | return Response( 99 | code: try getResponseCode(response, command: command), 100 | message: getResponseMessage(response), 101 | response: response 102 | ) 103 | } 104 | #else 105 | return try responsesArray.flatMap { response in 106 | guard response != "" else { 107 | return nil 108 | } 109 | return Response( 110 | code: try getResponseCode(response, command: command), 111 | message: getResponseMessage(response), 112 | response: response 113 | ) 114 | } 115 | #endif 116 | } 117 | 118 | func getResponseCode(_ response: String, command: Command) throws -> ResponseCode { 119 | guard response.count > 3 else { 120 | throw SMTPError.badResponse(command: command.text, response: response) 121 | } 122 | guard let code = Int(response[.. 2, 127 | command.expectedResponseCodes.map({ $0.rawValue }).contains(code) else { 128 | throw SMTPError.badResponse(command: command.text, response: response) 129 | } 130 | return ResponseCode(code) 131 | } 132 | 133 | func getResponseMessage(_ response: String) -> String { 134 | guard response.count > 3 else { 135 | return "" 136 | } 137 | return String(response[response.index(response.startIndex, offsetBy: 4)...]) 138 | } 139 | } 140 | 141 | private extension SMTPSocket { 142 | func getServerOptions(domainName: String) throws -> [Response] { 143 | do { 144 | return try send(.ehlo(domainName)) 145 | } catch { 146 | return try send(.helo(domainName)) 147 | } 148 | } 149 | 150 | func getAuthMethod(authMethods: [String: AuthMethod], serverOptions: [Response], hostname: String) throws -> AuthMethod { 151 | for option in serverOptions { 152 | let components = option.message.components(separatedBy: " ") 153 | if components.first == "AUTH" { 154 | let _authMethods = components.dropFirst() 155 | for authMethod in _authMethods { 156 | if let matchingAuthMethod = authMethods[authMethod] { 157 | return matchingAuthMethod 158 | } 159 | } 160 | } 161 | } 162 | throw SMTPError.noAuthMethodsOrRequiresTLS(hostname: hostname) 163 | } 164 | 165 | func doStarttls(serverOptions: [Response], tlsConfiguration: TLSConfiguration?) throws -> Bool { 166 | for option in serverOptions { 167 | if option.message == "STARTTLS" { 168 | try starttls(tlsConfiguration: tlsConfiguration) 169 | return true 170 | } 171 | } 172 | return false 173 | } 174 | 175 | func starttls(tlsConfiguration: TLSConfiguration?) throws { 176 | try send(.starttls) 177 | // Upgrade the socket to SSL/TLS 178 | if let tlsConfiguration = tlsConfiguration { 179 | socket.delegate = try tlsConfiguration.makeSSLService() 180 | } else { 181 | socket.delegate = try TLSConfiguration().makeSSLService() 182 | } 183 | try socket.delegate?.initialize(asServer: false) 184 | try socket.delegate?.onConnect(socket: socket) 185 | } 186 | 187 | func login(authMethod: AuthMethod, email: String, password: String) throws { 188 | switch authMethod { 189 | case .cramMD5: 190 | try loginCramMD5(email: email, password: password) 191 | case .login: 192 | try loginLogin(email: email, password: password) 193 | case .plain: 194 | try loginPlain(email: email, password: password) 195 | case .xoauth2: 196 | try loginXOAuth2(email: email, accessToken: password) 197 | } 198 | } 199 | 200 | func loginCramMD5(email: String, password: String) throws { 201 | let challenge = try auth(authMethod: .cramMD5, credentials: nil).message 202 | try authPassword(try AuthEncoder.cramMD5(challenge: challenge, user: email, password: password)) 203 | } 204 | 205 | func loginLogin(email: String, password: String) throws { 206 | try auth(authMethod: .login, credentials: nil) 207 | let credentials = AuthEncoder.login(user: email, password: password) 208 | try authUser(credentials.encodedUser) 209 | try authPassword(credentials.encodedPassword) 210 | } 211 | 212 | func loginPlain(email: String, password: String) throws { 213 | try auth( 214 | authMethod: .plain, 215 | credentials: AuthEncoder.plain(user: email, password: password) 216 | ) 217 | } 218 | 219 | func loginXOAuth2(email: String, accessToken: String) throws { 220 | try auth(authMethod: .xoauth2, credentials: AuthEncoder.xoauth2(user: email, accessToken: accessToken)) 221 | } 222 | 223 | @discardableResult 224 | func auth(authMethod: AuthMethod, credentials: String?) throws -> Response { 225 | let responses = try send(.auth(authMethod, credentials)) 226 | guard let response = responses.first else { 227 | throw SMTPError.badResponse(command: "AUTH", response: responses.description) 228 | } 229 | return response 230 | } 231 | 232 | func authUser(_ user: String) throws { 233 | try send(.authUser(user)) 234 | } 235 | 236 | func authPassword(_ password: String) throws { 237 | try send(.authPassword(password)) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/DataSender.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | import LoggerAPI 19 | 20 | // Used to send the content of an email--headers, text, and attachments. 21 | // Should only be invoked after sending the `DATA` command to the server. 22 | // The email is not actually sent until we have indicated that we are done sending its contents with a `CRLF CRLF`. 23 | // This is handled by `Sender`. 24 | struct DataSender { 25 | // Socket we use to read and write data to 26 | private let socket: SMTPSocket 27 | 28 | // Init a new instance of `DataSender` 29 | init(socket: SMTPSocket) { 30 | self.socket = socket 31 | } 32 | 33 | // Send the text and attachments of the `mail` 34 | func send(_ mail: Mail) throws { 35 | try sendHeaders(mail.headersString) 36 | 37 | if mail.hasAttachment { 38 | try sendMixed(mail) 39 | } else { 40 | try sendText(mail.text) 41 | } 42 | } 43 | } 44 | 45 | extension DataSender { 46 | // Send the headers of a `Mail` 47 | func sendHeaders(_ headers: String) throws { 48 | try send(headers) 49 | } 50 | 51 | // Add custom/default headers to a `Mail`'s text and write it to the socket. 52 | func sendText(_ text: String) throws { 53 | try send(text.embedded) 54 | } 55 | 56 | // Send `mail`'s content that is more than just plain text 57 | func sendMixed(_ mail: Mail) throws { 58 | let boundary = String.makeBoundary() 59 | let mixedHeader = String.makeMixedHeader(boundary: boundary) 60 | 61 | try send(mixedHeader) 62 | try send(boundary.startLine) 63 | 64 | try sendAlternative(for: mail) 65 | 66 | try sendAttachments(mail.attachments, boundary: boundary) 67 | } 68 | 69 | // If `mail` has an attachment that is an alternative to plain text, sends that attachment and the plain text. 70 | // Else just sends the plain text. 71 | func sendAlternative(for mail: Mail) throws { 72 | if let alternative = mail.alternative { 73 | let boundary = String.makeBoundary() 74 | let alternativeHeader = String.makeAlternativeHeader(boundary: boundary) 75 | try send(alternativeHeader) 76 | 77 | try send(boundary.startLine) 78 | try sendText(mail.text) 79 | 80 | try send(boundary.startLine) 81 | try sendAttachment(alternative) 82 | 83 | try send(boundary.endLine) 84 | return 85 | } 86 | 87 | try sendText(mail.text) 88 | } 89 | 90 | // Sends the attachments of a `Mail`. 91 | func sendAttachments(_ attachments: [Attachment], boundary: String) throws { 92 | for attachment in attachments { 93 | try send(boundary.startLine) 94 | try sendAttachment(attachment) 95 | } 96 | try send(boundary.endLine) 97 | } 98 | 99 | // Send the `attachment`. 100 | func sendAttachment(_ attachment: Attachment) throws { 101 | var relatedBoundary = "" 102 | 103 | if attachment.hasRelated { 104 | relatedBoundary = String.makeBoundary() 105 | let relatedHeader = String.makeRelatedHeader(boundary: relatedBoundary) 106 | try send(relatedHeader) 107 | try send(relatedBoundary.startLine) 108 | } 109 | 110 | let attachmentHeader = attachment.headersString + CRLF 111 | try send(attachmentHeader) 112 | 113 | switch attachment.type { 114 | case .data(let data, _, _, _): try sendData(data) 115 | case .file(let path, _, _, _): try sendFile(at: path) 116 | case .html(let content, _, _): try sendHTML(content) 117 | } 118 | 119 | try send("") 120 | 121 | if attachment.hasRelated { 122 | try sendAttachments(attachment.relatedAttachments, boundary: relatedBoundary) 123 | } 124 | } 125 | 126 | // Send a data attachment. Data must be base 64 encoded before sending. 127 | // Checks if the base 64 encoded version has been cached first. 128 | func sendData(_ data: Data) throws { 129 | #if os(macOS) 130 | if let encodedData = cache.object(forKey: data as AnyObject) as? Data { 131 | return try send(encodedData) 132 | } 133 | #else 134 | if let encodedData = cache.object(forKey: NSData(data: data) as AnyObject) as? Data { 135 | return try send(encodedData) 136 | } 137 | #endif 138 | 139 | let encodedData = data.base64EncodedData(options: .lineLength76Characters) 140 | try send(encodedData) 141 | 142 | #if os(macOS) 143 | cache.setObject(encodedData as AnyObject, forKey: data as AnyObject) 144 | #else 145 | cache.setObject(NSData(data: encodedData) as AnyObject, forKey: NSData(data: data) as AnyObject) 146 | #endif 147 | } 148 | 149 | // Sends a local file at the given path. File must be base 64 encoded before sending. Checks the cache first. 150 | // Throws an error if file could not be found. 151 | func sendFile(at path: String) throws { 152 | #if os(macOS) 153 | if let data = cache.object(forKey: path as AnyObject) as? Data { 154 | return try send(data) 155 | } 156 | #else 157 | if let data = cache.object(forKey: NSString(string: path) as AnyObject) as? Data { 158 | return try send(data) 159 | } 160 | #endif 161 | 162 | guard let file = FileHandle(forReadingAtPath: path) else { 163 | throw SMTPError.fileNotFound(path: path) 164 | } 165 | 166 | let data = file.readDataToEndOfFile().base64EncodedData(options: .lineLength76Characters) 167 | try send(data) 168 | file.closeFile() 169 | 170 | #if os(macOS) 171 | cache.setObject(data as AnyObject, forKey: path as AnyObject) 172 | #else 173 | cache.setObject(NSData(data: data) as AnyObject, forKey: NSString(string: path) as AnyObject) 174 | #endif 175 | } 176 | 177 | // Send an HTML attachment. HTML must be base 64 encoded before sending. 178 | // Checks if the base 64 encoded version is in cache first. 179 | func sendHTML(_ html: String) throws { 180 | #if os(macOS) 181 | if let encodedHTML = cache.object(forKey: html as AnyObject) as? String { 182 | return try send(encodedHTML) 183 | } 184 | #else 185 | if let encodedHTML = cache.object(forKey: NSString(string: html) as AnyObject) as? String { 186 | return try send(encodedHTML) 187 | } 188 | #endif 189 | 190 | let encodedHTML = html.data(using: .utf8)?.base64EncodedData(options: .lineLength76Characters) ?? Data() 191 | try send(encodedHTML) 192 | 193 | #if os(macOS) 194 | cache.setObject(encodedHTML as AnyObject, forKey: html as AnyObject) 195 | #else 196 | cache.setObject(NSData(data: encodedHTML) as AnyObject, forKey: NSString(string: html) as AnyObject) 197 | #endif 198 | } 199 | } 200 | 201 | private extension DataSender { 202 | // Write `text` to the socket. 203 | func send(_ text: String) throws { 204 | Log.debug("SEND: \(text)") 205 | try socket.write(text) 206 | } 207 | 208 | // Write `data` to the socket. 209 | func send(_ data: Data) throws { 210 | Log.debug("SEND: data \(data.count) bytes") 211 | try socket.write(data) 212 | } 213 | } 214 | 215 | private extension String { 216 | // Embed plain text content of emails with the proper headers so that it is entered correctly. 217 | var embedded: String { 218 | var embeddedText = "" 219 | embeddedText += "CONTENT-TYPE: text/plain; charset=utf-8\(CRLF)" 220 | embeddedText += "CONTENT-TRANSFER-ENCODING: 7bit\(CRLF)" 221 | embeddedText += "CONTENT-DISPOSITION: inline\(CRLF)" 222 | embeddedText += "\(CRLF)\(self)\(CRLF)" 223 | return embeddedText 224 | } 225 | 226 | // The SMTP protocol requires unique boundaries between sections of an email. 227 | static func makeBoundary() -> String { 228 | return UUID().uuidString.replacingOccurrences(of: "-", with: "") 229 | } 230 | 231 | // Header for a mixed type email. 232 | static func makeMixedHeader(boundary: String) -> String { 233 | return "CONTENT-TYPE: multipart/mixed; boundary=\"\(boundary)\"\(CRLF)" 234 | } 235 | 236 | // Header for an alternative email. 237 | static func makeAlternativeHeader(boundary: String) -> String { 238 | return "CONTENT-TYPE: multipart/alternative; boundary=\"\(boundary)\"\(CRLF)" 239 | } 240 | 241 | // Header for an attachment that is related to another attachment. (Such as an image attachment that can be 242 | // referenced by a related HTML attachment) 243 | static func makeRelatedHeader(boundary: String) -> String { 244 | return "CONTENT-TYPE: multipart/related; boundary=\"\(boundary)\"\(CRLF)" 245 | } 246 | 247 | // Added to a boundary to indicate the beginning of the corresponding section. 248 | var startLine: String { 249 | return "--\(self)" 250 | } 251 | 252 | // Added to a boundary to indicate the end of the corresponding section. 253 | var endLine: String { 254 | return "--\(self)--" 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Sources/SwiftSMTP/Attachment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corporation 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | import Foundation 18 | 19 | /// Represents a `Mail`'s attachment. 20 | /// Different SMTP servers have different attachment size limits. 21 | public struct Attachment { 22 | let type: AttachmentType 23 | let additionalHeaders: [String: String] 24 | let relatedAttachments: [Attachment] 25 | 26 | /// Initialize a data `Attachment`. 27 | /// 28 | /// - Parameters: 29 | /// - data: Raw data to be sent as attachment. 30 | /// - mime: MIME type of the data. 31 | /// - name: File name which will be presented in the mail. 32 | /// - inline: Indicates if attachment is inline. To embed the attachment 33 | /// in mail content, set to `true`. To send as standalone 34 | /// attachment, set to `false`. Defaults to `false`. 35 | /// - additionalHeaders: Additional headers for the `Mail`. Header keys 36 | /// are capitalized and duplicate keys will 37 | /// overwrite each other. Defaults to none. The 38 | /// following will be ignored: CONTENT-TYPE, 39 | /// CONTENT-DISPOSITION, CONTENT-TRANSFER-ENCODING. 40 | /// - related: Related `Attachment`s of this attachment. Defaults to 41 | /// none. 42 | public init(data: Data, 43 | mime: String, 44 | name: String, 45 | inline: Bool = false, 46 | additionalHeaders: [String: String] = [:], 47 | relatedAttachments: [Attachment] = []) { 48 | self.init(type: .data(data: data, 49 | mime: mime, 50 | name: name, 51 | inline: inline), 52 | additionalHeaders: additionalHeaders, 53 | relatedAttachments: relatedAttachments) 54 | } 55 | 56 | /// Initialize an `Attachment` from a local file. 57 | /// 58 | /// - Parameters: 59 | /// - filePath: Path to the local file. 60 | /// - mime: MIME type of the file. Defaults to 61 | /// `application/octet-stream`. 62 | /// - name: Name of the file. Defaults to the name component in its 63 | /// file path. 64 | /// - inline: Indicates if attachment is inline. To embed the attachment 65 | /// in mail content, set to `true`. To send as standalone 66 | /// attachment, set to `false`. Defaults to `false`. 67 | /// - additionalHeaders: Additional headers for the attachment. Header 68 | /// keys are capitalized and duplicate keys will 69 | /// replace each other. Defaults to none. 70 | /// - related: Related `Attachment`s of this attachment. Defaults to 71 | /// none. 72 | public init(filePath: String, 73 | mime: String = "application/octet-stream", 74 | name: String? = nil, 75 | inline: Bool = false, 76 | additionalHeaders: [String: String] = [:], 77 | relatedAttachments: [Attachment] = []) { 78 | let name = name ?? NSString(string: filePath).lastPathComponent 79 | self.init(type: .file(path: filePath, 80 | mime: mime, 81 | name: name, 82 | inline: inline), 83 | additionalHeaders: additionalHeaders, 84 | relatedAttachments: relatedAttachments) 85 | } 86 | 87 | /// Initialize an HTML `Attachment`. 88 | /// 89 | /// - Parameters: 90 | /// - htmlContent: Content string of HTML. 91 | /// - characterSet: Character encoding of `htmlContent`. Defaults to 92 | /// `utf-8`. 93 | /// - alternative: Whether the HTML is an alternative for plain text or 94 | /// not. Defaults to `true`. 95 | /// - additionalHeaders: Additional headers for the attachment. Header 96 | /// keys are capitalized and duplicate keys will 97 | /// replace each other. Defaults to none. 98 | /// - related: Related `Attachment`s of this attachment. Defaults to 99 | /// none. 100 | public init(htmlContent: String, 101 | characterSet: String = "utf-8", 102 | alternative: Bool = true, 103 | additionalHeaders: [String: String] = [:], 104 | relatedAttachments: [Attachment] = []) { 105 | self.init(type: .html(content: htmlContent, 106 | characterSet: characterSet, 107 | alternative: alternative), 108 | additionalHeaders: additionalHeaders, 109 | relatedAttachments: relatedAttachments) 110 | } 111 | 112 | private init(type: AttachmentType, 113 | additionalHeaders: [String: String], 114 | relatedAttachments: [Attachment]) { 115 | self.type = type 116 | self.additionalHeaders = additionalHeaders 117 | self.relatedAttachments = relatedAttachments 118 | } 119 | } 120 | 121 | extension Attachment { 122 | enum AttachmentType { 123 | case data(data: Data, mime: String, name: String, inline: Bool) 124 | case file(path: String, mime: String, name: String, inline: Bool) 125 | case html(content: String, characterSet: String, alternative: Bool) 126 | } 127 | } 128 | 129 | extension Attachment { 130 | private var headersDictionary: [String: String] { 131 | var dictionary = [String: String]() 132 | switch type { 133 | 134 | case .data(_, let mime, let name, let inline): 135 | dictionary["CONTENT-TYPE"] = mime 136 | var attachmentDisposition = inline ? "inline" : "attachment" 137 | if let mimeName = name.mimeEncoded { 138 | attachmentDisposition.append("; filename=\"\(mimeName)\"") 139 | } 140 | dictionary["CONTENT-DISPOSITION"] = attachmentDisposition 141 | 142 | case .file(_, let mime, let name, let inline): 143 | dictionary["CONTENT-TYPE"] = mime 144 | var attachmentDisposition = inline ? "inline" : "attachment" 145 | if let mimeName = name.mimeEncoded { 146 | attachmentDisposition.append("; filename=\"\(mimeName)\"") 147 | } 148 | dictionary["CONTENT-DISPOSITION"] = attachmentDisposition 149 | 150 | case .html(_, let characterSet, _): 151 | dictionary["CONTENT-TYPE"] = "text/html; charset=\(characterSet)" 152 | dictionary["CONTENT-DISPOSITION"] = "inline" 153 | } 154 | 155 | dictionary["CONTENT-TRANSFER-ENCODING"] = "BASE64" 156 | 157 | for (key, value) in additionalHeaders { 158 | let keyUppercased = key.uppercased() 159 | if keyUppercased != "CONTENT-TYPE" && 160 | keyUppercased != "CONTENT-DISPOSITION" && 161 | keyUppercased != "CONTENT-TRANSFER-ENCODING" { 162 | dictionary[keyUppercased] = value 163 | } 164 | } 165 | 166 | return dictionary 167 | } 168 | 169 | var headersString: String { 170 | return headersDictionary.map { (key, value) in 171 | return "\(key): \(value)" 172 | }.joined(separator: CRLF) 173 | } 174 | } 175 | 176 | extension Attachment { 177 | var hasRelated: Bool { 178 | return !relatedAttachments.isEmpty 179 | } 180 | 181 | var isAlternative: Bool { 182 | if case .html(_, _, let alternative) = type { 183 | return alternative 184 | } 185 | return false 186 | } 187 | } 188 | 189 | extension Attachment: Equatable { 190 | /// Returns `true` if the `Attachment`s are equal. 191 | public static func ==(lhs: Attachment, rhs: Attachment) -> Bool { 192 | return lhs.additionalHeaders == rhs.additionalHeaders && 193 | lhs.hasRelated == rhs.hasRelated && 194 | lhs.headersDictionary == rhs.headersDictionary && 195 | lhs.isAlternative == rhs.isAlternative && 196 | lhs.relatedAttachments == rhs.relatedAttachments && 197 | lhs.type == rhs.type 198 | } 199 | } 200 | 201 | extension Attachment.AttachmentType: Equatable { 202 | static func ==(lhs: Attachment.AttachmentType, rhs: Attachment.AttachmentType) -> Bool { 203 | switch (lhs, rhs) { 204 | case (let .data(data1, mime1, name1, inline1), let .data(data2, mime2, name2, inline2)): 205 | return data1 == data2 && 206 | mime1 == mime2 && 207 | name1 == name2 && 208 | inline1 == inline2 209 | case (let .file(path1, mime1, name1, inline1), let .file(path2, mime2, name2, inline2)): 210 | return path1 == path2 && 211 | mime1 == mime2 && 212 | name1 == name2 && 213 | inline1 == inline2 214 | case (let .html(content1, characterSet1, alternative1), 215 | let .html(content2, characterSet2, alternative2)): 216 | return content1 == content2 && 217 | characterSet1 == characterSet2 && 218 | alternative1 == alternative2 219 | default: 220 | return false 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #4183c4; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | a.discouraged { 67 | text-decoration: line-through; } 68 | a.discouraged:hover, a.discouraged:focus { 69 | text-decoration: underline line-through; } 70 | 71 | table { 72 | background: #fff; 73 | width: 100%; 74 | border-collapse: collapse; 75 | border-spacing: 0; 76 | overflow: auto; 77 | margin: 0 0 0.85em; } 78 | 79 | tr:nth-child(2n) { 80 | background-color: #fbfbfb; } 81 | 82 | th, td { 83 | padding: 6px 13px; 84 | border: 1px solid #ddd; } 85 | 86 | pre { 87 | margin: 0 0 1.275em; 88 | padding: .85em 1em; 89 | overflow: auto; 90 | background: #f7f7f7; 91 | font-size: .85em; 92 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 93 | 94 | code { 95 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 96 | 97 | .item-container p > code, .item-container li > code, .top-matter p > code, .top-matter li > code { 98 | background: #f7f7f7; 99 | padding: .2em; } 100 | .item-container p > code:before, .item-container p > code:after, .item-container li > code:before, .item-container li > code:after, .top-matter p > code:before, .top-matter p > code:after, .top-matter li > code:before, .top-matter li > code:after { 101 | letter-spacing: -.2em; 102 | content: "\00a0"; } 103 | 104 | pre code { 105 | padding: 0; 106 | white-space: pre; } 107 | 108 | .content-wrapper { 109 | display: flex; 110 | flex-direction: column; } 111 | @media (min-width: 768px) { 112 | .content-wrapper { 113 | flex-direction: row; } } 114 | .header { 115 | display: flex; 116 | padding: 8px; 117 | font-size: 0.875em; 118 | background: #444; 119 | color: #999; } 120 | 121 | .header-col { 122 | margin: 0; 123 | padding: 0 8px; } 124 | 125 | .header-col--primary { 126 | flex: 1; } 127 | 128 | .header-link { 129 | color: #fff; } 130 | 131 | .header-icon { 132 | padding-right: 6px; 133 | vertical-align: -4px; 134 | height: 16px; } 135 | 136 | .breadcrumbs { 137 | font-size: 0.875em; 138 | padding: 8px 16px; 139 | margin: 0; 140 | background: #fbfbfb; 141 | border-bottom: 1px solid #ddd; } 142 | 143 | .carat { 144 | height: 10px; 145 | margin: 0 5px; } 146 | 147 | .navigation { 148 | order: 2; } 149 | @media (min-width: 768px) { 150 | .navigation { 151 | order: 1; 152 | width: 25%; 153 | max-width: 300px; 154 | padding-bottom: 64px; 155 | overflow: hidden; 156 | word-wrap: normal; 157 | background: #fbfbfb; 158 | border-right: 1px solid #ddd; } } 159 | .nav-groups { 160 | list-style-type: none; 161 | padding-left: 0; } 162 | 163 | .nav-group-name { 164 | border-bottom: 1px solid #ddd; 165 | padding: 8px 0 8px 16px; } 166 | 167 | .nav-group-name-link { 168 | color: #333; } 169 | 170 | .nav-group-tasks { 171 | margin: 8px 0; 172 | padding: 0 0 0 8px; } 173 | 174 | .nav-group-task { 175 | font-size: 1em; 176 | list-style-type: none; 177 | white-space: nowrap; } 178 | 179 | .nav-group-task-link { 180 | color: #808080; } 181 | 182 | .main-content { 183 | order: 1; } 184 | @media (min-width: 768px) { 185 | .main-content { 186 | order: 2; 187 | flex: 1; 188 | padding-bottom: 60px; } } 189 | .section { 190 | padding: 0 32px; 191 | border-bottom: 1px solid #ddd; } 192 | 193 | .section-content { 194 | max-width: 834px; 195 | margin: 0 auto; 196 | padding: 16px 0; } 197 | 198 | .section-name { 199 | color: #666; 200 | display: block; } 201 | .section-name p { 202 | margin-bottom: inherit; } 203 | 204 | .declaration .highlight { 205 | overflow-x: initial; 206 | padding: 8px 0; 207 | margin: 0; 208 | background-color: transparent; 209 | border: none; } 210 | 211 | .task-group-section { 212 | border-top: 1px solid #ddd; } 213 | 214 | .task-group { 215 | padding-top: 0px; } 216 | 217 | .task-name-container a[name]:before { 218 | content: ""; 219 | display: block; } 220 | 221 | .section-name-container { 222 | position: relative; } 223 | .section-name-container .section-name-link { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | bottom: 0; 228 | right: 0; 229 | margin-bottom: 0; } 230 | .section-name-container .section-name { 231 | position: relative; 232 | pointer-events: none; 233 | z-index: 1; } 234 | .section-name-container .section-name a { 235 | pointer-events: auto; } 236 | 237 | .item-container { 238 | padding: 0; } 239 | 240 | .item { 241 | padding-top: 8px; 242 | width: 100%; 243 | list-style-type: none; } 244 | .item a[name]:before { 245 | content: ""; 246 | display: block; } 247 | .item .token, .item .direct-link { 248 | display: inline-block; 249 | text-indent: -20px; 250 | padding-left: 3px; 251 | margin-left: 20px; 252 | font-size: 1rem; } 253 | .item .declaration-note { 254 | font-size: .85em; 255 | color: #808080; 256 | font-style: italic; } 257 | 258 | .pointer-container { 259 | border-bottom: 1px solid #ddd; 260 | left: -23px; 261 | padding-bottom: 13px; 262 | position: relative; 263 | width: 110%; } 264 | 265 | .pointer { 266 | left: 21px; 267 | top: 7px; 268 | display: block; 269 | position: absolute; 270 | width: 12px; 271 | height: 12px; 272 | border-left: 1px solid #ddd; 273 | border-top: 1px solid #ddd; 274 | background: #fff; 275 | transform: rotate(45deg); } 276 | 277 | .height-container { 278 | display: none; 279 | position: relative; 280 | width: 100%; 281 | overflow: hidden; } 282 | .height-container .section { 283 | background: #fff; 284 | border: 1px solid #ddd; 285 | border-top-width: 0; 286 | padding-top: 10px; 287 | padding-bottom: 5px; 288 | padding: 8px 16px; } 289 | 290 | .aside, .language { 291 | padding: 6px 12px; 292 | margin: 12px 0; 293 | border-left: 5px solid #dddddd; 294 | overflow-y: hidden; } 295 | .aside .aside-title, .language .aside-title { 296 | font-size: 9px; 297 | letter-spacing: 2px; 298 | text-transform: uppercase; 299 | padding-bottom: 0; 300 | margin: 0; 301 | color: #aaa; 302 | -webkit-user-select: none; } 303 | .aside p:last-child, .language p:last-child { 304 | margin-bottom: 0; } 305 | 306 | .language { 307 | border-left: 5px solid #cde9f4; } 308 | .language .aside-title { 309 | color: #4183c4; } 310 | 311 | .aside-warning, .aside-deprecated, .aside-unavailable { 312 | border-left: 5px solid #ff6666; } 313 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 314 | color: #ff0000; } 315 | 316 | .graybox { 317 | border-collapse: collapse; 318 | width: 100%; } 319 | .graybox p { 320 | margin: 0; 321 | word-break: break-word; 322 | min-width: 50px; } 323 | .graybox td { 324 | border: 1px solid #ddd; 325 | padding: 5px 25px 5px 10px; 326 | vertical-align: middle; } 327 | .graybox tr td:first-of-type { 328 | text-align: right; 329 | padding: 7px; 330 | vertical-align: top; 331 | word-break: normal; 332 | width: 40px; } 333 | 334 | .slightly-smaller { 335 | font-size: 0.9em; } 336 | 337 | .footer { 338 | padding: 8px 16px; 339 | background: #444; 340 | color: #ddd; 341 | font-size: 0.8em; } 342 | .footer p { 343 | margin: 8px 0; } 344 | .footer a { 345 | color: #fff; } 346 | 347 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 348 | display: none; } 349 | 350 | html.dash .height-container { 351 | display: block; } 352 | 353 | form[role=search] input { 354 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 355 | font-size: 14px; 356 | line-height: 24px; 357 | padding: 0 10px; 358 | margin: 0; 359 | border: none; 360 | border-radius: 1em; } 361 | .loading form[role=search] input { 362 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 363 | 364 | form[role=search] .tt-menu { 365 | margin: 0; 366 | min-width: 300px; 367 | background: #fbfbfb; 368 | color: #333; 369 | border: 1px solid #ddd; } 370 | 371 | form[role=search] .tt-highlight { 372 | font-weight: bold; } 373 | 374 | form[role=search] .tt-suggestion { 375 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 376 | padding: 0 8px; } 377 | form[role=search] .tt-suggestion span { 378 | display: table-cell; 379 | white-space: nowrap; } 380 | form[role=search] .tt-suggestion .doc-parent-name { 381 | width: 100%; 382 | text-align: right; 383 | font-weight: normal; 384 | font-size: 0.9em; 385 | padding-left: 16px; } 386 | 387 | form[role=search] .tt-suggestion:hover, 388 | form[role=search] .tt-suggestion.tt-cursor { 389 | cursor: pointer; 390 | background-color: #4183c4; 391 | color: #fff; } 392 | 393 | form[role=search] .tt-suggestion:hover .doc-parent-name, 394 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 395 | color: #fff; } 396 | -------------------------------------------------------------------------------- /docs/docsets/SwiftSMTP.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #4183c4; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | a.discouraged { 67 | text-decoration: line-through; } 68 | a.discouraged:hover, a.discouraged:focus { 69 | text-decoration: underline line-through; } 70 | 71 | table { 72 | background: #fff; 73 | width: 100%; 74 | border-collapse: collapse; 75 | border-spacing: 0; 76 | overflow: auto; 77 | margin: 0 0 0.85em; } 78 | 79 | tr:nth-child(2n) { 80 | background-color: #fbfbfb; } 81 | 82 | th, td { 83 | padding: 6px 13px; 84 | border: 1px solid #ddd; } 85 | 86 | pre { 87 | margin: 0 0 1.275em; 88 | padding: .85em 1em; 89 | overflow: auto; 90 | background: #f7f7f7; 91 | font-size: .85em; 92 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 93 | 94 | code { 95 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 96 | 97 | .item-container p > code, .item-container li > code, .top-matter p > code, .top-matter li > code { 98 | background: #f7f7f7; 99 | padding: .2em; } 100 | .item-container p > code:before, .item-container p > code:after, .item-container li > code:before, .item-container li > code:after, .top-matter p > code:before, .top-matter p > code:after, .top-matter li > code:before, .top-matter li > code:after { 101 | letter-spacing: -.2em; 102 | content: "\00a0"; } 103 | 104 | pre code { 105 | padding: 0; 106 | white-space: pre; } 107 | 108 | .content-wrapper { 109 | display: flex; 110 | flex-direction: column; } 111 | @media (min-width: 768px) { 112 | .content-wrapper { 113 | flex-direction: row; } } 114 | .header { 115 | display: flex; 116 | padding: 8px; 117 | font-size: 0.875em; 118 | background: #444; 119 | color: #999; } 120 | 121 | .header-col { 122 | margin: 0; 123 | padding: 0 8px; } 124 | 125 | .header-col--primary { 126 | flex: 1; } 127 | 128 | .header-link { 129 | color: #fff; } 130 | 131 | .header-icon { 132 | padding-right: 6px; 133 | vertical-align: -4px; 134 | height: 16px; } 135 | 136 | .breadcrumbs { 137 | font-size: 0.875em; 138 | padding: 8px 16px; 139 | margin: 0; 140 | background: #fbfbfb; 141 | border-bottom: 1px solid #ddd; } 142 | 143 | .carat { 144 | height: 10px; 145 | margin: 0 5px; } 146 | 147 | .navigation { 148 | order: 2; } 149 | @media (min-width: 768px) { 150 | .navigation { 151 | order: 1; 152 | width: 25%; 153 | max-width: 300px; 154 | padding-bottom: 64px; 155 | overflow: hidden; 156 | word-wrap: normal; 157 | background: #fbfbfb; 158 | border-right: 1px solid #ddd; } } 159 | .nav-groups { 160 | list-style-type: none; 161 | padding-left: 0; } 162 | 163 | .nav-group-name { 164 | border-bottom: 1px solid #ddd; 165 | padding: 8px 0 8px 16px; } 166 | 167 | .nav-group-name-link { 168 | color: #333; } 169 | 170 | .nav-group-tasks { 171 | margin: 8px 0; 172 | padding: 0 0 0 8px; } 173 | 174 | .nav-group-task { 175 | font-size: 1em; 176 | list-style-type: none; 177 | white-space: nowrap; } 178 | 179 | .nav-group-task-link { 180 | color: #808080; } 181 | 182 | .main-content { 183 | order: 1; } 184 | @media (min-width: 768px) { 185 | .main-content { 186 | order: 2; 187 | flex: 1; 188 | padding-bottom: 60px; } } 189 | .section { 190 | padding: 0 32px; 191 | border-bottom: 1px solid #ddd; } 192 | 193 | .section-content { 194 | max-width: 834px; 195 | margin: 0 auto; 196 | padding: 16px 0; } 197 | 198 | .section-name { 199 | color: #666; 200 | display: block; } 201 | .section-name p { 202 | margin-bottom: inherit; } 203 | 204 | .declaration .highlight { 205 | overflow-x: initial; 206 | padding: 8px 0; 207 | margin: 0; 208 | background-color: transparent; 209 | border: none; } 210 | 211 | .task-group-section { 212 | border-top: 1px solid #ddd; } 213 | 214 | .task-group { 215 | padding-top: 0px; } 216 | 217 | .task-name-container a[name]:before { 218 | content: ""; 219 | display: block; } 220 | 221 | .section-name-container { 222 | position: relative; } 223 | .section-name-container .section-name-link { 224 | position: absolute; 225 | top: 0; 226 | left: 0; 227 | bottom: 0; 228 | right: 0; 229 | margin-bottom: 0; } 230 | .section-name-container .section-name { 231 | position: relative; 232 | pointer-events: none; 233 | z-index: 1; } 234 | .section-name-container .section-name a { 235 | pointer-events: auto; } 236 | 237 | .item-container { 238 | padding: 0; } 239 | 240 | .item { 241 | padding-top: 8px; 242 | width: 100%; 243 | list-style-type: none; } 244 | .item a[name]:before { 245 | content: ""; 246 | display: block; } 247 | .item .token, .item .direct-link { 248 | display: inline-block; 249 | text-indent: -20px; 250 | padding-left: 3px; 251 | margin-left: 20px; 252 | font-size: 1rem; } 253 | .item .declaration-note { 254 | font-size: .85em; 255 | color: #808080; 256 | font-style: italic; } 257 | 258 | .pointer-container { 259 | border-bottom: 1px solid #ddd; 260 | left: -23px; 261 | padding-bottom: 13px; 262 | position: relative; 263 | width: 110%; } 264 | 265 | .pointer { 266 | left: 21px; 267 | top: 7px; 268 | display: block; 269 | position: absolute; 270 | width: 12px; 271 | height: 12px; 272 | border-left: 1px solid #ddd; 273 | border-top: 1px solid #ddd; 274 | background: #fff; 275 | transform: rotate(45deg); } 276 | 277 | .height-container { 278 | display: none; 279 | position: relative; 280 | width: 100%; 281 | overflow: hidden; } 282 | .height-container .section { 283 | background: #fff; 284 | border: 1px solid #ddd; 285 | border-top-width: 0; 286 | padding-top: 10px; 287 | padding-bottom: 5px; 288 | padding: 8px 16px; } 289 | 290 | .aside, .language { 291 | padding: 6px 12px; 292 | margin: 12px 0; 293 | border-left: 5px solid #dddddd; 294 | overflow-y: hidden; } 295 | .aside .aside-title, .language .aside-title { 296 | font-size: 9px; 297 | letter-spacing: 2px; 298 | text-transform: uppercase; 299 | padding-bottom: 0; 300 | margin: 0; 301 | color: #aaa; 302 | -webkit-user-select: none; } 303 | .aside p:last-child, .language p:last-child { 304 | margin-bottom: 0; } 305 | 306 | .language { 307 | border-left: 5px solid #cde9f4; } 308 | .language .aside-title { 309 | color: #4183c4; } 310 | 311 | .aside-warning, .aside-deprecated, .aside-unavailable { 312 | border-left: 5px solid #ff6666; } 313 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 314 | color: #ff0000; } 315 | 316 | .graybox { 317 | border-collapse: collapse; 318 | width: 100%; } 319 | .graybox p { 320 | margin: 0; 321 | word-break: break-word; 322 | min-width: 50px; } 323 | .graybox td { 324 | border: 1px solid #ddd; 325 | padding: 5px 25px 5px 10px; 326 | vertical-align: middle; } 327 | .graybox tr td:first-of-type { 328 | text-align: right; 329 | padding: 7px; 330 | vertical-align: top; 331 | word-break: normal; 332 | width: 40px; } 333 | 334 | .slightly-smaller { 335 | font-size: 0.9em; } 336 | 337 | .footer { 338 | padding: 8px 16px; 339 | background: #444; 340 | color: #ddd; 341 | font-size: 0.8em; } 342 | .footer p { 343 | margin: 8px 0; } 344 | .footer a { 345 | color: #fff; } 346 | 347 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 348 | display: none; } 349 | 350 | html.dash .height-container { 351 | display: block; } 352 | 353 | form[role=search] input { 354 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 355 | font-size: 14px; 356 | line-height: 24px; 357 | padding: 0 10px; 358 | margin: 0; 359 | border: none; 360 | border-radius: 1em; } 361 | .loading form[role=search] input { 362 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 363 | 364 | form[role=search] .tt-menu { 365 | margin: 0; 366 | min-width: 300px; 367 | background: #fbfbfb; 368 | color: #333; 369 | border: 1px solid #ddd; } 370 | 371 | form[role=search] .tt-highlight { 372 | font-weight: bold; } 373 | 374 | form[role=search] .tt-suggestion { 375 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 376 | padding: 0 8px; } 377 | form[role=search] .tt-suggestion span { 378 | display: table-cell; 379 | white-space: nowrap; } 380 | form[role=search] .tt-suggestion .doc-parent-name { 381 | width: 100%; 382 | text-align: right; 383 | font-weight: normal; 384 | font-size: 0.9em; 385 | padding-left: 16px; } 386 | 387 | form[role=search] .tt-suggestion:hover, 388 | form[role=search] .tt-suggestion.tt-cursor { 389 | cursor: pointer; 390 | background-color: #4183c4; 391 | color: #fff; } 392 | 393 | form[role=search] .tt-suggestion:hover .doc-parent-name, 394 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 395 | color: #fff; } 396 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | --------------------------------------------------------------------------------