├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── AddressParser.swift └── Tests ├── AddressParserTests └── AddressParserTests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift,xcode,xcode,objective-c,osx 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xcuserstate 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | 72 | ### Xcode ### 73 | # Xcode 74 | # 75 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 76 | 77 | ## Build generated 78 | 79 | ## Various settings 80 | 81 | ## Other 82 | *.xccheckout 83 | *.xcscmblueprint 84 | 85 | 86 | ### Xcode ### 87 | # Xcode 88 | # 89 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 90 | 91 | ## Build generated 92 | 93 | ## Various settings 94 | 95 | ## Other 96 | 97 | 98 | ### Objective-C ### 99 | # Xcode 100 | # 101 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 102 | 103 | ## Build generated 104 | 105 | ## Various settings 106 | 107 | ## Other 108 | 109 | ## Obj-C/Swift specific 110 | 111 | # CocoaPods 112 | # 113 | # We recommend against adding the Pods directory to your .gitignore. However 114 | # you should judge for yourself, the pros and cons are mentioned at: 115 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 116 | # 117 | # Pods/ 118 | 119 | # Carthage 120 | # 121 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 122 | # Carthage/Checkouts 123 | 124 | 125 | # fastlane 126 | # 127 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 128 | # screenshots whenever they are needed. 129 | # For more information about the recommended setup visit: 130 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 131 | 132 | 133 | # Code Injection 134 | # 135 | # After new code Injection tools there's a generated folder /iOSInjectionProject 136 | # https://github.com/johnno1962/injectionforxcode 137 | 138 | iOSInjectionProject/ 139 | 140 | ### Objective-C Patch ### 141 | 142 | 143 | ### OSX ### 144 | *.DS_Store 145 | .AppleDouble 146 | .LSOverride 147 | 148 | # Icon must end with two \r 149 | Icon 150 | # Thumbnails 151 | ._* 152 | # Files that might appear in the root of a volume 153 | .DocumentRevisions-V100 154 | .fseventsd 155 | .Spotlight-V100 156 | .TemporaryItems 157 | .Trashes 158 | .VolumeIcon.icns 159 | .com.apple.timemachine.donotpresent 160 | # Directories potentially created on remote AFP share 161 | .AppleDB 162 | .AppleDesktop 163 | Network Trash Folder 164 | Temporary Items 165 | .apdisk 166 | 167 | # End of https://www.gitignore.io/api/swift,xcode,xcode,objective-c,osx 168 | /AddressParser.xcodeproj 169 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Wei Wang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // 3 | // Package.swift 4 | // AddressParser 5 | // 6 | // Created by Wei Wang on 2017/01/01. 7 | // 8 | // Copyright (c) 2017 Wei Wang 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | // THE SOFTWARE. 27 | 28 | import PackageDescription 29 | 30 | let package = Package( 31 | name: "AddressParser", 32 | products: [ 33 | .library( 34 | name: "AddressParser", 35 | targets: ["AddressParser"] 36 | ), 37 | ], 38 | dependencies: [], 39 | targets: [ 40 | .target( 41 | name: "AddressParser", 42 | dependencies: [], 43 | path: "Sources" 44 | ), 45 | .testTarget( 46 | name: "AddressParserTests", 47 | dependencies: ["AddressParser"] 48 | ), 49 | ], 50 | swiftLanguageVersions: [.version("5")] 51 | ) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AddressParser 2 | 3 | This framework is a component of [Hedwig](https://github.com/onevcat/Hedwig), 4 | which is a cross platform Swift SMTP email client framework. 5 | 6 | When sending an email, you need to specify the receipt address. The address 7 | field could be one of these forms below: 8 | 9 | * Single address: `onev@onevcat.com` 10 | * Formatted address with name: `Wei Wang ` 11 | * A list of addresses: `onev@onevcat.com, foo@bar.com` 12 | * A group: `My Group: onev@onevcat.com, foo@bar.com;` 13 | 14 | and even more and mixed... 15 | 16 | If you need a complete solution of sending mails through SMTP by Swift, see 17 | [Hedwig](https://github.com/onevcat/Hedwig) instead. 18 | 19 | ## Installation 20 | 21 | Add the url of this repo to your `Package.swift`: 22 | 23 | ```swift 24 | import PackageDescription 25 | 26 | let package = Package( 27 | name: "YourAwesomeSoftware", 28 | dependencies: [ 29 | .Package(url: "https://github.com/onevcat/AddressParser.git", 30 | majorVersion: 1) 31 | ] 32 | ) 33 | ``` 34 | 35 | Then run `swift build` whenever you get prepared. 36 | 37 | You could know more information on how to use Swift Package Manager in Apple's 38 | [official page](https://swift.org/package-manager/). 39 | 40 | ## Usage 41 | 42 | Use `AddressParser.parse` to parse an email string field to an array of `Address`. 43 | An `Address` struct contains the name of that address and an entry to indicate 44 | whether this is a mail address or a group. 45 | 46 | ```swift 47 | import AddressParser 48 | 49 | let _ = AddressParser.parse("onev@onevcat.com") 50 | // [Address(name: "", entry: .mail("onev@onevcat.com"))] 51 | 52 | let _ = AddressParser.parse("Wei Wang ") 53 | // [Address(name: "Wei Wang", entry: .mail("onev@onevcat.com"))] 54 | 55 | let _ = AddressParser.parse("onev@onevcat.com, foo@bar.com") 56 | // [ 57 | // Address(name: "", entry: .mail("onev@onevcat.com")) 58 | // Address(name: "", entry: .mail("foo@bar.com")) 59 | // ] 60 | 61 | let _ = AddressParser.parse("My Group: onev@onevcat.com, foo@bar.com;") 62 | // [ 63 | // Address(name: "MyGroup", entry: .group([ 64 | // Address(name: "", entry: .mail("onev@onevcat.com")), 65 | // Address(name: "", entry: .mail("foo@bar.com")), 66 | // ])) 67 | // ] 68 | ``` 69 | 70 | ## License 71 | 72 | MIT. See the LICENSE file. 73 | 74 | -------------------------------------------------------------------------------- /Sources/AddressParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressParser.swift 3 | // AddressParser 4 | // 5 | // Created by Wei Wang on 2017/01/01. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | #if os(Linux) 30 | #if swift(>=3.1) 31 | typealias Regex = NSRegularExpression 32 | #else 33 | typealias Regex = RegularExpression 34 | #endif 35 | #else 36 | typealias Regex = NSRegularExpression 37 | #endif 38 | 39 | public struct Address { 40 | public let name: String 41 | public let entry: Entry 42 | 43 | public init(name: String, entry: Entry) { 44 | self.name = name 45 | self.entry = entry 46 | } 47 | } 48 | 49 | extension Address: Equatable { 50 | public static func ==(lhs: Address, rhs: Address) -> Bool { 51 | lhs.name == rhs.name && lhs.entry == rhs.entry 52 | } 53 | } 54 | 55 | public indirect enum Entry { 56 | case mail(String) 57 | case group([Address]) 58 | } 59 | 60 | extension Entry: Equatable { 61 | public static func ==(lhs: Entry, rhs: Entry) -> Bool { 62 | switch (lhs, rhs) { 63 | case (.mail(let address1), .mail(let address2)): return address1 == address2 64 | case (.group(let addresses1), .group(let addresses2)): return addresses1 == addresses2 65 | default: return false 66 | } 67 | } 68 | } 69 | 70 | public enum AddressParser { 71 | enum Node { 72 | case op(String) 73 | case text(String) 74 | } 75 | 76 | private enum ParsingState: Int { 77 | case address 78 | case comment 79 | case group 80 | case text 81 | } 82 | 83 | public static func parse(_ text: String) -> [Address] { 84 | var address = [Node]() 85 | var addresses = [[Node]]() 86 | 87 | let nodes = Tokenizer(text: text).tokenize() 88 | nodes.forEach { node in 89 | if case .op(let value) = node, value == "," || value == ";" { 90 | if !address.isEmpty { 91 | addresses.append(address) 92 | } 93 | address = [] 94 | } else { 95 | address.append(node) 96 | } 97 | } 98 | 99 | if !address.isEmpty { 100 | addresses.append(address) 101 | } 102 | 103 | return addresses.compactMap(parseAddress) 104 | } 105 | 106 | static func parseAddress(address: [Node]) -> Address? { 107 | var parsing: [ParsingState: [String]] = [ 108 | .address: [], 109 | .comment: [], 110 | .group: [], 111 | .text: [] 112 | ] 113 | 114 | func parsingIsEmpty(_ state: ParsingState) -> Bool { 115 | parsing[state]?.isEmpty == true 116 | } 117 | 118 | var state: ParsingState = .text 119 | var isGroup = false 120 | 121 | for node in address { 122 | if case .op(let op) = node { 123 | switch op { 124 | case "<": 125 | state = .address 126 | case "(": 127 | state = .comment 128 | case ":": 129 | state = .group 130 | isGroup = true 131 | default: 132 | state = .text 133 | } 134 | } else if case .text(var value) = node { 135 | if state == .address { 136 | value = value.truncateUnexpectedLessThanOp() 137 | } 138 | parsing[state]?.append(value) 139 | } 140 | } 141 | 142 | // If there is no text but a comment, use comment for text instead. 143 | if parsingIsEmpty(.text), !parsingIsEmpty(.comment) { 144 | parsing[.text] = parsing[.comment] 145 | parsing[.comment] = [] 146 | } 147 | 148 | if isGroup { 149 | // http://tools.ietf.org/html/rfc2822#appendix-A.1.3 150 | let name = parsing[.text]?.joined(separator: " ") ?? "" 151 | let group = parsingIsEmpty(.group) ? [] : parse(parsing[.group]?.joined(separator: ",") ?? "") 152 | return Address(name: name, entry: .group(group)) 153 | } else { 154 | // No address found but there is text. Try to find an address from text. 155 | if parsingIsEmpty(.address), !parsingIsEmpty(.text) { 156 | for text in parsing[.text]?.reversed() ?? [] { 157 | if text.isEmail, let found = parsing[.text]?.removeLastMatch(text) { 158 | parsing[.address]?.append(found) 159 | break 160 | } 161 | } 162 | } 163 | 164 | // Did not find an address in text. Try again with a looser condition. 165 | if parsingIsEmpty(.address) { 166 | var textHolder = [String]() 167 | for text in parsing[.text]?.reversed() ?? [] { 168 | if parsingIsEmpty(.address) { 169 | let result = text.replacingMatch(regex: .looserMailRegex, with: "") 170 | textHolder.append(result.afterReplacing) 171 | if let matched = result.matched { 172 | parsing[.address] = [matched.trimmingCharacters(in: .whitespaces)] 173 | } 174 | } else { 175 | textHolder.append(text) 176 | } 177 | } 178 | parsing[.text] = textHolder.reversed() 179 | } 180 | 181 | // If there is still no text but a comment, use comment for text instead. 182 | if parsingIsEmpty(.text), !parsingIsEmpty(.comment) { 183 | parsing[.text] = parsing[.comment] 184 | parsing[.comment] = [] 185 | } 186 | 187 | if var addresses = parsing[.address], addresses.count > 1 { 188 | let keepAddress = addresses.removeFirst() 189 | parsing[.text]?.append(contentsOf: addresses) 190 | parsing[.address] = [keepAddress] 191 | } 192 | 193 | let tempText = parsing[.text]?.joined(separator: " ").nilOnEmpty 194 | 195 | // Remove single/douch quote mark in addresses 196 | let tempAddress = parsing[.address]?.joined(separator: " ").trimmingQuote.nilOnEmpty 197 | 198 | if address.isEmpty, isGroup { 199 | return nil 200 | } else { 201 | var address = tempAddress ?? tempText ?? "" 202 | var name = tempText ?? tempAddress ?? "" 203 | if address == name { 204 | if address.contains("@") { 205 | name = "" 206 | } else { 207 | address = "" 208 | } 209 | } 210 | 211 | return Address(name: name, entry: .mail(address)) 212 | } 213 | } 214 | } 215 | 216 | class Tokenizer { 217 | let operators: [Character: Character?] = 218 | ["\"": "\"", "(": ")", "<": ">", 219 | ",": nil, ":": ";", ";": nil] 220 | 221 | let text: String 222 | var currentOp: Character? 223 | var expectingOp: Character? 224 | var escaped = false 225 | 226 | var currentNode: Node? 227 | var list = [Node]() 228 | 229 | init(text: String) { 230 | self.text = text 231 | } 232 | 233 | func tokenize() -> [Node] { 234 | text.forEach(check) 235 | appendCurrentNode() 236 | 237 | return list.filter { (node) -> Bool in 238 | let value: String 239 | switch node { 240 | case .op(let op): value = op 241 | case .text(let text): value = text 242 | } 243 | return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 244 | } 245 | } 246 | 247 | func check(char: Character) { 248 | if operators.keys.contains(char) || char == "\\", escaped { 249 | escaped = false 250 | } else if char == expectingOp { 251 | appendCurrentNode() 252 | list.append(.op(String(char))) 253 | expectingOp = nil 254 | escaped = false 255 | return 256 | } else if expectingOp == nil, let expecting = operators[char] { 257 | appendCurrentNode() 258 | list.append(.op(String(char))) 259 | expectingOp = expecting 260 | escaped = false 261 | return 262 | } 263 | 264 | if !escaped, char == "\\" { 265 | escaped = true 266 | return 267 | } 268 | 269 | if currentNode == nil { 270 | currentNode = .text("") 271 | } 272 | 273 | if case .text(var currentText) = currentNode { 274 | if escaped, char != "\\" { 275 | currentText.append("\\") 276 | } 277 | currentText.append(char) 278 | currentNode = .text(currentText) 279 | escaped = false 280 | } 281 | } 282 | 283 | func appendCurrentNode() { 284 | if let currentNode = currentNode { 285 | switch currentNode { 286 | case .op(let value): 287 | list.append(.op(value.trimmingCharacters(in: .whitespacesAndNewlines))) 288 | case .text(let value): 289 | list.append(.text(value.trimmingCharacters(in: .whitespacesAndNewlines))) 290 | } 291 | } 292 | currentNode = nil 293 | } 294 | } 295 | } 296 | 297 | extension Regex { 298 | static let lessThanOpRegex = (try? Regex(pattern: "^[^<]*<\\s*", options: [])) ?? Regex() 299 | static let emailRegex = (try? Regex(pattern: "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}", options: [])) ?? Regex() 300 | static let quoteRegex = (try? Regex(pattern: "^(\"|\'){1}.+@.+(\"|\'){1}$", options: [])) ?? Regex() 301 | static let looserMailRegex = (try? Regex(pattern: "\\s*\\b[^@\\s]+@[^\\s]+\\b\\s*", options: [])) ?? Regex() 302 | } 303 | 304 | extension String { 305 | func truncateUnexpectedLessThanOp() -> String { 306 | let range = NSMakeRange(0, utf16.count) 307 | return Regex.lessThanOpRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "") 308 | } 309 | 310 | var isEmail: Bool { 311 | let range = NSMakeRange(0, utf16.count) 312 | return !Regex.emailRegex.matches(in: self, options: [], range: range).isEmpty 313 | } 314 | 315 | var trimmingQuote: String { 316 | let range = NSMakeRange(0, utf16.count) 317 | let result = Regex.quoteRegex.matches(in: self, options: [], range: range) 318 | 319 | if !result.isEmpty { 320 | let r = NSMakeRange(1, utf16.count - 2) 321 | return NSString(string: self).substring(with: r) 322 | } else { 323 | return self 324 | } 325 | } 326 | 327 | func replacingMatch(regex: Regex, with replace: String) -> (afterReplacing: String, matched: String?) { 328 | let range = NSMakeRange(0, utf16.count) 329 | let matches = regex.matches(in: self, options: [], range: range) 330 | 331 | guard let firstMatch = matches.first else { 332 | return (self, nil) 333 | } 334 | 335 | let matched = NSString(string: self).substring(with: firstMatch.range) 336 | let afterReplacing = NSString(string: self).replacingCharacters(in: firstMatch.range, with: replace) 337 | 338 | return (afterReplacing, matched) 339 | } 340 | 341 | var nilOnEmpty: String? { 342 | isEmpty ? nil : self 343 | } 344 | } 345 | 346 | extension Array where Element: Equatable { 347 | mutating func removeLastMatch(_ item: Element) -> Element? { 348 | guard let index = Array(reversed()).firstIndex(of: item) else { 349 | return nil 350 | } 351 | return remove(at: count - 1 - index) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /Tests/AddressParserTests/AddressParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddressParserTests.swift 3 | // AddressParser 4 | // 5 | // Created by Wei Wang on 2017/01/01. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import XCTest 28 | import AddressParser 29 | 30 | class AddressParserTests: XCTestCase { 31 | func testCanParseSingleAddress() { 32 | let addresses = AddressParser.parse("onev@onevcat.com") 33 | let expected = [Address(name: "", entry: .mail("onev@onevcat.com"))] 34 | XCTAssertEqual(addresses, expected) 35 | } 36 | 37 | func testCanParseMultipleAddresses() { 38 | let addresses = AddressParser.parse("onev@onevcat.com, foo@bar.com") 39 | let expected = [ 40 | Address(name: "", entry: .mail("onev@onevcat.com")), 41 | Address(name: "", entry: .mail("foo@bar.com")) 42 | ] 43 | XCTAssertEqual(addresses, expected) 44 | } 45 | 46 | func testCanParseUnquotedName() { 47 | let addresses = AddressParser.parse("onevcat ") 48 | let expected = [ 49 | Address(name: "onevcat", entry: .mail("onev@onevcat.com")) 50 | ] 51 | XCTAssertEqual(addresses, expected) 52 | } 53 | 54 | func testCanParseQuotedName() { 55 | let addresses = AddressParser.parse("\"wei wang\" ") 56 | let expected = [ 57 | Address(name: "wei wang", entry: .mail("onev@onevcat.com")) 58 | ] 59 | XCTAssertEqual(addresses, expected) 60 | } 61 | 62 | func testCanParseQuotedSemicolonName() { 63 | let addresses = AddressParser.parse("\"wei; wang\" ") 64 | let expected = [ 65 | Address(name: "wei; wang", entry: .mail("onev@onevcat.com")) 66 | ] 67 | XCTAssertEqual(addresses, expected) 68 | } 69 | 70 | func testCanParseSingleQuote() { 71 | let addresses = AddressParser.parse("wei wang <'onev@onevcat.com'>") 72 | let expected = [Address(name: "wei wang", entry: .mail("onev@onevcat.com"))] 73 | XCTAssertEqual(addresses, expected) 74 | } 75 | 76 | func testCanParseDoubleQuote() { 77 | let addresses = AddressParser.parse("wei wang <\"onev@onevcat.com\">") 78 | let expected = [Address(name: "wei wang", entry: .mail("onev@onevcat.com"))] 79 | XCTAssertEqual(addresses, expected) 80 | } 81 | 82 | func testCanPaeseQuoteBoth() { 83 | let addresses = AddressParser.parse("\"onev@onevcat.com\" <\"onev@onevcat.com\">") 84 | let expected = [Address(name: "", entry: .mail("onev@onevcat.com"))] 85 | XCTAssertEqual(addresses, expected) 86 | } 87 | 88 | func testCanPaeseQuoteInline() { 89 | let addresses = AddressParser.parse("\"onev@onevcat.com\" ") 90 | let expected = [Address(name: "onev@onevcat.com", entry: .mail("onev@onev\"cat.com\""))] 91 | XCTAssertEqual(addresses, expected) 92 | } 93 | 94 | func testCanParseUnquotedNameAddress() { 95 | let addresses = AddressParser.parse("onevcat onev@onevcat.com") 96 | let expected = [ 97 | Address(name: "onevcat", entry: .mail("onev@onevcat.com")) 98 | ] 99 | XCTAssertEqual(addresses, expected) 100 | } 101 | 102 | func testCanParseEmptyGroup() { 103 | let addresses = AddressParser.parse("EmptyGroup:;") 104 | let expected = [ 105 | Address(name: "EmptyGroup", entry: .group([])) 106 | ] 107 | XCTAssertEqual(addresses, expected) 108 | } 109 | 110 | func testCanParseGroup() { 111 | let addresses = AddressParser.parse("MyGroup: onev@onevcat.com, foo@bar.com;") 112 | let expected = [ 113 | Address(name: "MyGroup", entry: .group([ 114 | Address(name: "", entry: .mail("onev@onevcat.com")), 115 | Address(name: "", entry: .mail("foo@bar.com")), 116 | ])) 117 | ] 118 | XCTAssertEqual(addresses, expected) 119 | } 120 | 121 | func testCanRecognizeSemicolon() { 122 | let addresses = AddressParser.parse("onev@onevcat.com; foo@bar.com") 123 | let expected = [ 124 | Address(name: "", entry: .mail("onev@onevcat.com")), 125 | Address(name: "", entry: .mail("foo@bar.com")) 126 | ] 127 | XCTAssertEqual(addresses, expected) 128 | } 129 | 130 | func testCanParseMixedSingleAndGroup() { 131 | let addresses = AddressParser.parse("Wei Wang , \"My Group\": onev@onevcat.com, foo@bar.com;,, EmptyGroup:;") 132 | let expected = [ 133 | Address(name: "Wei Wang", entry: .mail("onev@onevcat.com")), 134 | Address(name: "My Group", entry: .group([ 135 | Address(name: "", entry: .mail("onev@onevcat.com")), 136 | Address(name: "", entry: .mail("foo@bar.com")), 137 | ])), 138 | Address(name: "EmptyGroup", entry: .group([])) 139 | ] 140 | XCTAssertEqual(addresses, expected) 141 | } 142 | 143 | func testCanParseSemicolonInMixed() { 144 | let addresses = AddressParser.parse("Wei Wang ; \"My Group\": onev@onevcat.com, foo@bar.com;,, EmptyGroup:; foo@bar.com") 145 | let expected = [ 146 | Address(name: "Wei Wang", entry: .mail("onev@onevcat.com")), 147 | Address(name: "My Group", entry: .group([ 148 | Address(name: "", entry: .mail("onev@onevcat.com")), 149 | Address(name: "", entry: .mail("foo@bar.com")), 150 | ])), 151 | Address(name: "EmptyGroup", entry: .group([])), 152 | Address(name: "", entry: .mail("foo@bar.com")) 153 | ] 154 | XCTAssertEqual(addresses, expected) 155 | } 156 | 157 | func testCanParseNameFromComment() { 158 | let addresses = AddressParser.parse("onev@onevcat.com (onevcat)") 159 | let expected = [Address(name: "onevcat", entry: .mail("onev@onevcat.com"))] 160 | XCTAssertEqual(addresses, expected) 161 | } 162 | 163 | func testCanSkipUnnecessaryComment() { 164 | let addresses = AddressParser.parse("onev@onevcat.com (wei wang) onevcat") 165 | let expected = [Address(name: "onevcat", entry: .mail("onev@onevcat.com"))] 166 | XCTAssertEqual(addresses, expected) 167 | } 168 | 169 | func testCanParseMissingAddress() { 170 | let addresses = AddressParser.parse("onevcat") 171 | let expected = [Address(name: "onevcat", entry: .mail(""))] 172 | XCTAssertEqual(addresses, expected) 173 | } 174 | 175 | func testCanParseNameApostrophe() { 176 | let addresses = AddressParser.parse("OneV'sDen") 177 | let expected = [Address(name: "OneV'sDen", entry: .mail(""))] 178 | XCTAssertEqual(addresses, expected) 179 | } 180 | 181 | func testCanUnescapedColon() { 182 | let addresses = AddressParser.parse("FirstName Surname-WithADash :: Company ") 183 | let expected = [ 184 | Address(name: "FirstName Surname-WithADash", entry: .group([ 185 | Address(name: "", entry: .group([ 186 | Address(name: "Company", entry: .mail("firstname@company.com")) 187 | ])) 188 | ])) 189 | ] 190 | XCTAssertEqual(addresses, expected) 191 | } 192 | 193 | func testCanParseInvalidAddress() { 194 | let addresses = AddressParser.parse("onev@onevcat.com@onevdog.com") 195 | let expected = [Address(name: "", entry: .mail("onev@onevcat.com@onevdog.com"))] 196 | XCTAssertEqual(addresses, expected) 197 | } 198 | 199 | func testCanParseInvalidQuote() { 200 | let addresses = AddressParser.parse("wei wa>ng < onevcat ") 201 | let expected = [Address(name: "wei wa>ng", entry: .mail("onev@onevcat.com"))] 202 | XCTAssertEqual(addresses, expected) 203 | } 204 | 205 | 206 | static var allTests : [(String, (AddressParserTests) -> () throws -> Void)] { 207 | return [ 208 | ("testCanParseSingleAddress", testCanParseSingleAddress), 209 | ("testCanParseMultipleAddresses", testCanParseMultipleAddresses), 210 | ("testCanParseUnquotedName", testCanParseUnquotedName), 211 | ("testCanParseQuotedName", testCanParseQuotedName), 212 | ("testCanParseQuotedSemicolonName", testCanParseQuotedSemicolonName), 213 | ("testCanParseSingleQuote", testCanParseSingleQuote), 214 | ("testCanParseDoubleQuote", testCanParseDoubleQuote), 215 | ("testCanPaeseQuoteBoth", testCanPaeseQuoteBoth), 216 | ("testCanPaeseQuoteInline", testCanPaeseQuoteInline), 217 | ("testCanParseUnquotedNameAddress", testCanParseUnquotedNameAddress), 218 | ("testCanParseEmptyGroup", testCanParseEmptyGroup), 219 | ("testCanParseGroup", testCanParseGroup), 220 | ("testCanRecognizeSemicolon", testCanRecognizeSemicolon), 221 | ("testCanParseMixedSingleAndGroup", testCanParseMixedSingleAndGroup), 222 | ("testCanParseSemicolonInMixed", testCanParseSemicolonInMixed), 223 | ("testCanParseNameFromComment", testCanParseNameFromComment), 224 | ("testCanSkipUnnecessaryComment", testCanSkipUnnecessaryComment), 225 | ("testCanParseMissingAddress", testCanParseMissingAddress), 226 | ("testCanParseNameApostrophe", testCanParseNameApostrophe), 227 | ("testCanUnescapedColon", testCanUnescapedColon), 228 | ("testCanParseInvalidAddress", testCanParseInvalidAddress), 229 | ("testCanParseInvalidQuote", testCanParseInvalidQuote) 230 | ] 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AddressParserTests 3 | 4 | XCTMain([ 5 | testCase(AddressParserTests.allTests), 6 | ]) 7 | --------------------------------------------------------------------------------