UNPKG

7.1 kBJavaScriptView Raw
1
2// expose to the world
3module.exports = parser;
4
5/**
6 * Parses structured e-mail addresses from an address field
7 *
8 * Example:
9 *
10 * "Name <address@domain>"
11 *
12 * will be converted to
13 *
14 * [{name: "Name", address: "address@domain"}]
15 *
16 * @param {String} str Address field
17 * @return {Array} An array of address objects
18 */
19function parser(str){
20 var tokenizer = new Tokenizer(str),
21 tokens = tokenizer.tokenize();
22
23
24 var addresses = [],
25 address = [],
26 parsedAddresses = [];
27
28 tokens.forEach(function(token){
29 if(token.type == "operator" && (token.value =="," || token.value ==";")){
30 addresses.push(address);
31 address = [];
32 }else{
33 address.push(token);
34 }
35 });
36
37 if(address.length){
38 addresses.push(address);
39 }
40
41 addresses.forEach(function(address){
42 address = handleAddress(address);
43 if(address.length){
44 parsedAddresses = parsedAddresses.concat(address);
45 }
46 });
47
48 return parsedAddresses;
49}
50
51/**
52 * Converts tokens for a single address into an address object
53 *
54 * @param {Array} tokens Tokens object
55 * @return {Object} Address object
56 */
57function handleAddress(tokens){
58 var token,
59 isGroup = false,
60 state = "text",
61 address,
62 addresses = [],
63 data = {
64 address: [],
65 comment: [],
66 group: [],
67 text: []
68 },
69 i, len;
70
71 // Filter out <addresses>, (comments) and regular text
72 for(i=0, len = tokens.length; i<len; i++){
73 token = tokens[i];
74
75 if(token.type == "operator"){
76 switch(token.value){
77 case "<":
78 state = "address";
79 break;
80 case "(":
81 state = "comment";
82 break;
83 case ":":
84 state = "group";
85 isGroup = true;
86 break;
87 default:
88 state = "text";
89 }
90 }else{
91 if(token.value){
92 data[state].push(token.value);
93 }
94 }
95 }
96
97 // If there is no text but a comment, replace the two
98 if(!data.text.length && data.comment.length){
99 data.text = data.comment;
100 data.comment = [];
101 }
102
103 if(data.group.length){
104
105 if(data.text.length){
106 data.text = data.text.join(" ");
107 }
108
109 addresses = addresses.concat(parser(data.group.join(",")).map(function(address){
110 address.name = data.text || address.name;
111 return address;
112 }));
113
114 }else{
115 // If no address was found, try to detect one from regular text
116 if(!data.address.length && data.text.length){
117 for(i = data.text.length - 1; i>=0; i--){
118 if(data.text[i].match(/^[^@\s]+@[^@\s]+$/)){
119 data.address = data.text.splice(i,1);
120 break;
121 }
122 }
123
124 // still no address
125 if(!data.address.length){
126 for(i = data.text.length - 1; i>=0; i--){
127 data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, function(address){
128 if(!data.address.length){
129 data.address = [address.trim()];
130 return " ";
131 }else{
132 return address;
133 }
134 }).trim();
135 if(data.address.length){
136 break;
137 }
138 }
139 }
140 }
141
142 // If there's still is no text but a comment exixts, replace the two
143 if(!data.text.length && data.comment.length){
144 data.text = data.comment;
145 data.comment = [];
146 }
147
148 // Keep only the first address occurence, push others to regular text
149 if(data.address.length > 1){
150 data.text = data.text.concat(data.address.splice(1));
151 }
152
153 // Join values with spaces
154 data.text = data.text.join(" ");
155 data.address = data.address.join(" ");
156
157 if(!data.address && isGroup){
158 return [];
159 }else{
160 address = {
161 address: data.address || data.text,
162 name: data.text || data.address
163 };
164
165 if(address.address == address.name){
166 if((address.address || "").match(/@/)){
167 delete address.name;
168 }else{
169 delete address.address;
170 }
171
172 }
173
174 addresses.push(address);
175 }
176 }
177
178 return addresses;
179}
180
181
182/**
183 * Creates a TOkenizer object for tokenizing address field strings
184 *
185 * @constructor
186 * @param {String} str Address field string
187 */
188function Tokenizer(str){
189
190 this.str = (str || "").toString();
191 this.operatorCurrent = "";
192 this.operatorExpecting = "";
193 this.node = null;
194 this.escaped = false;
195
196 this.list = [];
197
198}
199
200/**
201 * Operator tokens and which tokens are expected to end the sequence
202 */
203Tokenizer.prototype.operators = {
204 "\"": "\"",
205 "'": "'",
206 "(": ")",
207 "<": ">",
208 ",": "",
209 ":": ";"
210};
211
212/**
213 * Tokenizes the original input string
214 *
215 * @return {Array} An array of operator|text tokens
216 */
217Tokenizer.prototype.tokenize = function(){
218 var chr, list = [];
219 for(var i=0, len = this.str.length; i<len; i++){
220 chr = this.str.charAt(i);
221 this.checkChar(chr);
222 }
223
224 this.list.forEach(function(node){
225 node.value = (node.value || "").toString().trim();
226 if(node.value){
227 list.push(node);
228 }
229 });
230
231 return list;
232};
233
234/**
235 * Checks if a character is an operator or text and acts accordingly
236 *
237 * @param {String} chr Character from the address field
238 */
239Tokenizer.prototype.checkChar = function(chr){
240 if((chr in this.operators || chr == "\\") && this.escaped){
241 this.escaped = false;
242 }else if(this.operatorExpecting && chr == this.operatorExpecting){
243 this.node = {
244 type: "operator",
245 value: chr
246 };
247 this.list.push(this.node);
248 this.node = null;
249 this.operatorExpecting = "";
250 this.escaped = false;
251 return;
252 }else if(!this.operatorExpecting && chr in this.operators){
253 this.node = {
254 type: "operator",
255 value: chr
256 };
257 this.list.push(this.node);
258 this.node = null;
259 this.operatorExpecting = this.operators[chr];
260 this.escaped = false;
261 return;
262 }
263
264 if(!this.escaped && chr == "\\"){
265 this.escaped = true;
266 return;
267 }
268
269 if(!this.node){
270 this.node = {
271 type: "text",
272 value: ""
273 };
274 this.list.push(this.node);
275 }
276
277 if(this.escaped && chr != "\\"){
278 this.node.value += "\\";
279 }
280
281 this.node.value += chr;
282 this.escaped = false;
283};