index.js 8.2 KB

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