├── .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 |
--------------------------------------------------------------------------------