|
| 1 | +/* eslint-disable node/no-deprecated-api */ |
| 2 | +/* eslint-disable no-control-regex */ |
| 3 | + |
| 4 | +import { flatten } from 'ramda' |
| 5 | +import parseAddress from 'emailjs-addressparser' |
| 6 | +import { |
| 7 | + mimeWordsEncode, |
| 8 | + mimeWordEncode, |
| 9 | + continuationEncode |
| 10 | +} from 'emailjs-mime-codec' |
| 11 | +import { toASCII } from 'punycode' |
| 12 | + |
| 13 | +/** |
| 14 | + * If needed, mime encodes the name part |
| 15 | + * |
| 16 | + * @param {String} name Name part of an address |
| 17 | + * @returns {String} Mime word encoded string if needed |
| 18 | + */ |
| 19 | +function encodeAddressName (name) { |
| 20 | + if (!/^[\w ']*$/.test(name)) { |
| 21 | + if (/^[\x20-\x7e]*$/.test(name)) { |
| 22 | + return '"' + name.replace(/([\\"])/g, '\\$1') + '"' |
| 23 | + } else { |
| 24 | + return mimeWordEncode(name, 'Q') |
| 25 | + } |
| 26 | + } |
| 27 | + return name |
| 28 | +} |
| 29 | + |
| 30 | +/** |
| 31 | + * Checks if a value is plaintext string (uses only printable 7bit chars) |
| 32 | + * |
| 33 | + * @param {String} value String to be tested |
| 34 | + * @returns {Boolean} true if it is a plaintext string |
| 35 | + */ |
| 36 | +export function isPlainText (value) { |
| 37 | + return !(typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) |
| 38 | +} |
| 39 | + |
| 40 | +/** |
| 41 | + * Rebuilds address object using punycode and other adjustments |
| 42 | + * |
| 43 | + * @param {Array} addresses An array of address objects |
| 44 | + * @param {Array} [uniqueList] An array to be populated with addresses |
| 45 | + * @return {String} address string |
| 46 | + */ |
| 47 | +export function convertAddresses (addresses = [], uniqueList = []) { |
| 48 | + var values = [] |
| 49 | + |
| 50 | + ;[].concat(addresses).forEach(address => { |
| 51 | + if (address.address) { |
| 52 | + address.address = address.address |
| 53 | + .replace(/^.*?(?=@)/, user => mimeWordsEncode(user, 'Q')) |
| 54 | + .replace(/@.+$/, domain => '@' + toASCII(domain.substr(1))) |
| 55 | + |
| 56 | + if (!address.name) { |
| 57 | + values.push(address.address) |
| 58 | + } else if (address.name) { |
| 59 | + values.push(encodeAddressName(address.name) + ' <' + address.address + '>') |
| 60 | + } |
| 61 | + |
| 62 | + if (uniqueList.indexOf(address.address) < 0) { |
| 63 | + uniqueList.push(address.address) |
| 64 | + } |
| 65 | + } else if (address.group) { |
| 66 | + values.push(encodeAddressName(address.name) + ':' + (address.group.length ? convertAddresses(address.group, uniqueList) : '').trim() + ';') |
| 67 | + } |
| 68 | + }) |
| 69 | + |
| 70 | + return values.join(', ') |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Parses addresses. Takes in a single address or an array or an |
| 75 | + * array of address arrays (eg. To: [[first group], [second group],...]) |
| 76 | + * |
| 77 | + * @param {Mixed} addresses Addresses to be parsed |
| 78 | + * @return {Array} An array of address objects |
| 79 | + */ |
| 80 | +export function parseAddresses (addresses = []) { |
| 81 | + return flatten([].concat(addresses).map((address) => { |
| 82 | + if (address && address.address) { |
| 83 | + address = convertAddresses(address) |
| 84 | + } |
| 85 | + return parseAddress(address) |
| 86 | + })) |
| 87 | +} |
| 88 | + |
| 89 | +/** |
| 90 | + * Encodes a header value for use in the generated rfc2822 email. |
| 91 | + * |
| 92 | + * @param {String} key Header key |
| 93 | + * @param {String} value Header value |
| 94 | + */ |
| 95 | +export function encodeHeaderValue (key, value = '') { |
| 96 | + key = normalizeHeaderKey(key) |
| 97 | + |
| 98 | + switch (key) { |
| 99 | + case 'From': |
| 100 | + case 'Sender': |
| 101 | + case 'To': |
| 102 | + case 'Cc': |
| 103 | + case 'Bcc': |
| 104 | + case 'Reply-To': |
| 105 | + return convertAddresses(parseAddresses(value)) |
| 106 | + |
| 107 | + case 'Message-Id': |
| 108 | + case 'In-Reply-To': |
| 109 | + case 'Content-Id': |
| 110 | + value = value.replace(/\r?\n|\r/g, ' ') |
| 111 | + |
| 112 | + if (value.charAt(0) !== '<') { |
| 113 | + value = '<' + value |
| 114 | + } |
| 115 | + |
| 116 | + if (value.charAt(value.length - 1) !== '>') { |
| 117 | + value = value + '>' |
| 118 | + } |
| 119 | + return value |
| 120 | + |
| 121 | + case 'References': |
| 122 | + value = [].concat.apply([], [].concat(value).map((elm = '') => elm |
| 123 | + .replace(/\r?\n|\r/g, ' ') |
| 124 | + .trim() |
| 125 | + .replace(/<[^>]*>/g, str => str.replace(/\s/g, '')) |
| 126 | + .split(/\s+/) |
| 127 | + )).map(function (elm) { |
| 128 | + if (elm.charAt(0) !== '<') { |
| 129 | + elm = '<' + elm |
| 130 | + } |
| 131 | + if (elm.charAt(elm.length - 1) !== '>') { |
| 132 | + elm = elm + '>' |
| 133 | + } |
| 134 | + return elm |
| 135 | + }) |
| 136 | + |
| 137 | + return value.join(' ').trim() |
| 138 | + |
| 139 | + default: |
| 140 | + return mimeWordsEncode((value || '').toString().replace(/\r?\n|\r/g, ' '), 'B') |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +/** |
| 145 | + * Normalizes a header key, uses Camel-Case form, except for uppercase MIME- |
| 146 | + * |
| 147 | + * @param {String} key Key to be normalized |
| 148 | + * @return {String} key in Camel-Case form |
| 149 | + */ |
| 150 | +export function normalizeHeaderKey (key = '') { |
| 151 | + return key.replace(/\r?\n|\r/g, ' ') // no newlines in keys |
| 152 | + .trim().toLowerCase() |
| 153 | + .replace(/^MIME\b|^[a-z]|-[a-z]/ig, c => c.toUpperCase()) // use uppercase words, except MIME |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Generates a multipart boundary value |
| 158 | + * |
| 159 | + * @return {String} boundary value |
| 160 | + */ |
| 161 | +export function generateBoundary (nodeId, baseBoundary) { |
| 162 | + return '----sinikael-?=_' + nodeId + '-' + baseBoundary |
| 163 | +} |
| 164 | + |
| 165 | +/** |
| 166 | + * Escapes a header argument value (eg. boundary value for content type), |
| 167 | + * adds surrounding quotes if needed |
| 168 | + * |
| 169 | + * @param {String} value Header argument value |
| 170 | + * @return {String} escaped and quoted (if needed) argument value |
| 171 | + */ |
| 172 | +export function escapeHeaderArgument (value) { |
| 173 | + if (value.match(/[\s'"\\;/=]|^-/g)) { |
| 174 | + return '"' + value.replace(/(["\\])/g, '\\$1') + '"' |
| 175 | + } else { |
| 176 | + return value |
| 177 | + } |
| 178 | +} |
| 179 | + |
| 180 | +/** |
| 181 | + * Joins parsed header value together as 'value; param1=value1; param2=value2' |
| 182 | + * |
| 183 | + * @param {Object} structured Parsed header value |
| 184 | + * @return {String} joined header value |
| 185 | + */ |
| 186 | +export function buildHeaderValue (structured) { |
| 187 | + var paramsArray = [] |
| 188 | + |
| 189 | + Object.keys(structured.params || {}).forEach(param => { |
| 190 | + // filename might include unicode characters so it is a special case |
| 191 | + if (param === 'filename') { |
| 192 | + continuationEncode(param, structured.params[param], 50).forEach(function (encodedParam) { |
| 193 | + // continuation encoded strings are always escaped, so no need to use enclosing quotes |
| 194 | + // in fact using quotes might end up with invalid filenames in some clients |
| 195 | + paramsArray.push(encodedParam.key + '=' + encodedParam.value) |
| 196 | + }) |
| 197 | + } else { |
| 198 | + paramsArray.push(param + '=' + escapeHeaderArgument(structured.params[param])) |
| 199 | + } |
| 200 | + }) |
| 201 | + |
| 202 | + return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '') |
| 203 | +} |
0 commit comments