mime-js.coffee (16495B)
1 ### 2 mime-js.js 0.2.0 3 2014-10-18 4 5 By Ikrom, https://github.com/ikr0m 6 License: X11/MIT 7 ### 8 9 window.Mime = do -> 10 11 # ********************************* 12 # Create Mime Text from Mail Object 13 14 # var mail = { 15 # "to": "email1@example.com, email2@example.com", 16 # "cc": "email3@example.com, email4@example.com", 17 # "subject": "Today is rainy", 18 # "fromName": "John Smith", 19 # "from": "john.smith@mail.com", 20 # "body": "Sample body text", 21 # "cids": [], 22 # "attaches" : [] 23 # } 24 toMimeTxt = (mail, txtOnly) -> 25 linkify = (inputText) -> 26 #URLs starting with http://, https://, or ftp:// 27 replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim 28 replacedText = inputText.replace(replacePattern1, 29 "<a href=\"$1\" target=\"_blank\">$1</a>") 30 31 #URLs starting with "www." (without // before it, or it'd re-link the ones done above). 32 replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim 33 replacedText = replacedText.replace(replacePattern2, 34 "$1<a href=\"http://$2\" target=\"_blank\">$2</a>") 35 36 replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim 37 replacedText = replacedText.replace(replacePattern3, 38 '<a href="mailto:$1">$1</a>') 39 40 replacedText 41 42 getBoundary = -> 43 _random = -> Math.random().toString(36).slice(2) 44 _random() + _random() 45 46 createPlain = (textContent = '') -> 47 '\nContent-Type: text/plain; charset=UTF-8' + 48 '\nContent-Transfer-Encoding: base64' + 49 '\n\n' + (Base64.encode textContent, true).replace(/.{76}/g, "$&\n") 50 51 createHtml = (msg) -> 52 htmlContent = msg.body || "" 53 htmlContent = htmlContent.replace(/&/g, '&').replace(/</g, '<') 54 .replace(/>/, '>').replace(/\n/g, '\n<br/>') 55 56 htmlContent = linkify(htmlContent) 57 58 htmlContent = '<div>' + htmlContent + '</div>' 59 '\nContent-Type: text/html; charset=UTF-8' + 60 '\nContent-Transfer-Encoding: base64' + 61 '\n\n' + (Base64.encode htmlContent, true).replace(/.{76}/g, "$&\n") 62 63 createAlternative = (text, html) -> 64 boundary = getBoundary() 65 66 '\nContent-Type: multipart/alternative; boundary=' + boundary + 67 '\n\n--' + boundary + text + 68 '\n\n--' + boundary + html + 69 '\n\n--' + boundary + '--' 70 71 createCids = (cids) -> 72 return if !cids 73 cidArr = [] 74 for cid in cids 75 type = cid.type 76 name = cid.name 77 base64 = cid.base64 78 id = getBoundary() 79 80 cidArr.push '\nContent-Type: ' + type + '; name=\"' + name + '\"' + 81 '\nContent-Transfer-Encoding: base64' + 82 '\nContent-ID: <' + id + '>' + 83 '\nX-Attachment-Id: ' + id + 84 '\n\n' + base64 85 cidArr 86 87 createRelated = (alternative, cids = []) -> 88 boundary = getBoundary() 89 90 relatedStr = '\nContent-Type: multipart/related; boundary=' + boundary + 91 '\n\n--' + boundary + alternative 92 for cid in cids 93 relatedStr += ('\n--' + boundary + cid) 94 95 relatedStr + '\n--' + boundary + '--' 96 97 createAttaches = (attaches) -> 98 return if !attaches 99 result = [] 100 for attach in attaches 101 type = attach.type 102 name = attach.name 103 id = getBoundary() 104 content = '' 105 part = '\nContent-Type: ' + type 106 if name 107 part += '; name=\"' + name + '\"' + 108 '\nContent-Disposition: attachment; filename=\"' + name + '\"' 109 if attach.base64 110 content = attach.base64 111 part += '\nContent-Transfer-Encoding: base64' 112 else 113 content = attach.raw 114 part += '\nX-Attachment-Id: ' + id 115 part += '\n\n' + content 116 result.push(part) 117 result 118 119 createMixed = (related, attaches) -> 120 boundary = getBoundary() 121 subject = '' 122 if mail.subject 123 subject = '=?UTF-8?B?' + Base64.encode(mail.subject, true) + '?=' 124 125 mailFromName = '=?UTF-8?B?' + Base64.encode(mail.fromName || "", 126 true) + '?=' 127 date = (new Date().toGMTString()).replace(/GMT|UTC/gi, '+0000') 128 mimeStr = 'MIME-Version: 1.0' + 129 '\nDate: ' + date + 130 '\nMessage-ID: <' + getBoundary() + '@mail.your-domain.com>' + 131 '\nSubject: ' + subject + 132 '\nFrom: ' + mailFromName + ' <' + mail.from + '>' + 133 (if mail.to then '\nTo: ' + mail.to else '') + 134 (if mail.cc then '\nCc: ' + mail.cc else '') + 135 '\nContent-Type: multipart/mixed; boundary=' + boundary + 136 '\n\n--' + boundary + related 137 138 for attach in attaches 139 mimeStr += ('\n--' + boundary + attach) 140 141 (mimeStr + '\n--' + boundary + '--').replace /\n/g, '\r\n' 142 143 144 plain = createPlain mail.body 145 if txtOnly 146 related = plain 147 else 148 htm = createHtml mail 149 alternative = createAlternative plain, htm 150 cids = createCids mail.cids 151 related = createRelated alternative, cids 152 153 attaches = createAttaches mail.attaches 154 155 result = createMixed(related, attaches) 156 157 result 158 159 160 # ********************************* 161 # MailParser helper 162 163 MailParser = (rawMessage) -> 164 explodeMessage = (inMessage) -> 165 inHeaderPos = inMessage.indexOf("\r\n\r\n") 166 if inHeaderPos is -1 167 inMessage = inMessage.replace(/\n/g, "\r\n") # Let's give it a try 168 inHeaderPos = inMessage.indexOf("\r\n\r\n") 169 # empty body 170 inHeaderPos = inMessage.length if inHeaderPos is -1 171 172 inRawHeaders = inMessage.slice(0, inHeaderPos).replace(/\r\n\s+/g, " ") + "\r\n" 173 inRawBody = inMessage.slice(inHeaderPos).replace(/(\r\n)+$/, "").replace(/^(\r\n)+/, "") 174 inContentType = "" 175 regContentType = inRawHeaders.match(/Content-Type: (.*)/i) 176 177 if regContentType and regContentType.length > 0 178 inContentType = regContentType[1] # ignore case-sensitive Content-type 179 else 180 console.log "Warning: MailParser: Content-type doesn't exist!" 181 182 inContentTypeParts = inContentType.split(";") 183 mimeType = inContentTypeParts[0].replace(/\s/g, "") 184 mimeTypeParts = mimeType.split("/") 185 186 # If it's a multipart we need to split it up 187 if mimeTypeParts[0].toLowerCase() is "multipart" 188 inBodyParts = [] 189 190 #MS sends boundary in 3rd element 191 match = inContentTypeParts[1].match(/boundary="?([^"]*)"?/i) 192 match = inContentTypeParts[2].match(/boundary="?([^"]*)"?/i) if not match and inContentTypeParts[2] 193 inBoundary = _util.trim(match[1]).replace(/"/g, "") 194 escBoundary = inBoundary.replace(/\+/g, "\\+") # We should escape '+' sign 195 regString = new RegExp("--" + escBoundary, "g") 196 inBodyParts = inRawBody.replace(regString, inBoundary).replace(regString, inBoundary).split(inBoundary) 197 inBodyParts.shift() 198 inBodyParts.pop() 199 i = 0 200 201 while i < inBodyParts.length 202 inBodyParts[i] = inBodyParts[i].replace(/(\r\n)+$/, "").replace(/^(\r\n)+/, "") 203 inBodyParts[i] = explodeMessage(inBodyParts[i]) 204 i++ 205 else 206 inBody = inRawBody 207 if mimeTypeParts[0] is "text" 208 inBody = inBody.replace(RegExp("=\\r\\n", "g"), "") 209 specialChars = inBody.match(RegExp("=[A-F0-9][A-F0-9]", "g")) 210 if specialChars 211 i = 0 212 213 while i < specialChars.length 214 inBody = inBody.replace(specialChars[i], 215 String.fromCharCode(parseInt(specialChars[i].replace(RegExp("="), ""), 16))) 216 i++ 217 218 rawHeaders: inRawHeaders 219 rawBody: inRawBody 220 body: inBody 221 contentType: inContentType 222 contentTypeParts: inContentTypeParts 223 boundary: inBoundary 224 bodyParts: inBodyParts 225 mimeType: mimeType 226 mimeTypeParts: mimeTypeParts 227 228 messageParts = "" 229 try 230 messageParts = explodeMessage(rawMessage) 231 rawHeaders = messageParts.rawHeaders 232 getValidStr = (arr = []) -> 233 arr[1] or "" 234 235 subject = getValidStr((/\r\nSubject: (.*)\r\n/g).exec(rawHeaders)) 236 to = getValidStr((/\r\nTo: (.*)\r\n/g).exec(rawHeaders)) 237 cc = getValidStr((/\r\nCc: (.*)\r\n/g).exec(rawHeaders)) 238 from = getValidStr((/\r\nFrom: (.*)\r\n/g).exec(rawHeaders)) 239 240 { 241 messageParts: messageParts 242 subject: subject 243 to: to 244 cc: cc 245 from: from 246 } 247 248 249 # ****************************** 250 # Local Utility 251 252 _util = do -> 253 trim = (str = '') -> 254 str.trim?() || str.replace(/^\s+|\s+$/g, '') 255 256 decode = (txt = '', charset = '') -> 257 charset = charset.toLowerCase() 258 result = switch 259 when charset.indexOf('koi8-r') isnt -1 then KOIRDec(txt) 260 when charset.indexOf('utf-8') isnt -1 then Base64._utf8_decode(txt) 261 when charset.indexOf('windows-1251') isnt -1 then win1251Dec(txt) 262 else 263 txt 264 265 result 266 267 # QuotedPrintable Decode 268 QPDec = (s) -> 269 s.replace(/\=[\r\n]+/g, "").replace(/\=[0-9A-F]{2}/gi, (v) -> 270 String.fromCharCode(parseInt(v.substr(1), 16))) 271 272 KOIRDec = (str) -> 273 charmap = unescape( 274 "%u2500%u2502%u250C%u2510%u2514%u2518%u251C%u2524%u252C%u2534%u253C%u2580%u2584%u2588%u258C%u2590" + 275 "%u2591%u2592%u2593%u2320%u25A0%u2219%u221A%u2248%u2264%u2265%u00A0%u2321%u00B0%u00B2%u00B7%u00F7" + 276 "%u2550%u2551%u2552%u0451%u2553%u2554%u2555%u2556%u2557%u2558%u2559%u255A%u255B%u255C%u255D%u255E" + 277 "%u255F%u2560%u2561%u0401%u2562%u2563%u2564%u2565%u2566%u2567%u2568%u2569%u256A%u256B%u256C%u00A9" + 278 "%u044E%u0430%u0431%u0446%u0434%u0435%u0444%u0433%u0445%u0438%u0439%u043A%u043B%u043C%u043D%u043E" + 279 "%u043F%u044F%u0440%u0441%u0442%u0443%u0436%u0432%u044C%u044B%u0437%u0448%u044D%u0449%u0447%u044A" + 280 "%u042E%u0410%u0411%u0426%u0414%u0415%u0424%u0413%u0425%u0418%u0419%u041A%u041B%u041C%u041D%u041E" + 281 "%u041F%u042F%u0420%u0421%u0422%u0423%u0416%u0412%u042C%u042B%u0417%u0428%u042D%u0429%u0427%u042A") 282 code2char = (code) -> 283 return charmap.charAt(code - 0x80) if code >= 0x80 and code <= 0xFF 284 String.fromCharCode(code) 285 res = "" 286 for val, i in str 287 res = res + code2char str.charCodeAt i 288 289 res 290 291 win1251Dec = (str = '') -> 292 result = '' 293 for s, i in str 294 iCode = str.charCodeAt(i) 295 oCode = switch 296 when iCode is 168 then 1025 297 when iCode is 184 then 1105 298 when 191 < iCode < 256 then iCode + 848 299 else 300 iCode 301 result = result + String.fromCharCode(oCode) 302 303 result 304 305 _decodeMimeWord = (str, toCharset) -> 306 str = _util.trim(str) 307 fromCharset = undefined 308 encoding = undefined 309 match = undefined 310 match = str.match(/^\=\?([\w_\-]+)\?([QqBb])\?([^\?]*)\?\=$/i) 311 return decode(str, toCharset) unless match 312 313 fromCharset = match[1] 314 encoding = (match[2] or "Q").toString().toUpperCase() 315 str = (match[3] or "").replace(/_/g, " ") 316 if encoding is "B" 317 Base64.decode str, toCharset #, fromCharset 318 else if encoding is "Q" 319 QPDec str #, toCharset, fromCharset 320 else 321 str 322 323 decodeMimeWords = (str, toCharset) -> 324 # curCharset = undefined 325 str = (str or "").toString().replace(/(=\?[^?]+\?[QqBb]\?[^?]+\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, "$1") 326 .replace(/\=\?([\w_\-]+)\?([QqBb])\?[^\?]*\?\=/g, ((mimeWord, charset, encoding) -> 327 # curCharset = charset + encoding 328 _decodeMimeWord mimeWord #, curCharset 329 ).bind(this)) 330 331 decode str, toCharset 332 333 toHtmlEntity = (txt = "") -> 334 (txt + "").replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 335 336 {decode, KOIRDec, win1251Dec, decodeMimeWords, toHtmlEntity, trim} 337 338 339 # ********************************* 340 # Create Mail Object from Mime Text 341 342 343 buildMimeObj = (rawMailObj) -> 344 readyMail = 345 html: "" 346 text: "" 347 attaches: [] 348 innerMsgs: [] 349 to: _util.decodeMimeWords(rawMailObj.to) 350 cc: _util.decodeMimeWords(rawMailObj.cc) 351 from: _util.decodeMimeWords rawMailObj.from 352 subject: _util.decodeMimeWords rawMailObj.subject 353 354 decodeBody = (body, rawHeaders) -> 355 isQP = /Content-Transfer-Encoding: quoted-printable/i.test(rawHeaders) 356 isBase64 = /Content-Transfer-Encoding: base64/i.test(rawHeaders) 357 if isBase64 358 body = body.replace(/\s/g, '') 359 decBody = atob?(body) 360 decBody ?= Base64.decode(body) 361 body = decBody 362 else if isQP 363 body = _util.QPDec body 364 365 body 366 367 parseBodyParts = (bodyParts) -> 368 return if !bodyParts 369 for part in bodyParts 370 mimeType = (part.mimeType ? "").toLowerCase() 371 if mimeType.indexOf('multipart') isnt -1 372 parseBodyParts part.bodyParts 373 continue 374 375 if mimeType.indexOf('message/rfc822') isnt -1 376 newMimeMsg = MailParser(part.rawBody) 377 innerMsg = toMimeObj(newMimeMsg) 378 readyMail.innerMsgs.push innerMsg 379 # txt = innerMsg.text 380 # htm = innerMsg.html 381 # readyMail.text += txt if txt 382 # readyMail.html += htm if htm 383 # if innerMsg.attaches?.length > 0 384 # readyMail.attaches = readyMail.attaches.concat(innerMsg.attaches) 385 continue 386 387 rawHeaders = part.rawHeaders 388 isAttach = rawHeaders.indexOf('Content-Disposition: attachment') isnt -1 389 body = part.rawBody 390 391 isHtml = /text\/html/.test(mimeType) 392 isPlain = /text\/plain/.test(mimeType) 393 isImg = /image/.test(mimeType) 394 isAudio = /audio/.test(mimeType) 395 # isBase64 = /Content-Transfer-Encoding: base64/i.test(rawHeaders) 396 397 if isAttach or isImg or isAudio 398 isQP = /Content-Transfer-Encoding: quoted-printable/i.test(rawHeaders) 399 if isQP 400 body = _util.QPDec body 401 body = if btoa then btoa(body) else Base64.encode(body) 402 403 # name = null 404 for typePart in part.contentTypeParts 405 if /name=/i.test(typePart) 406 name = typePart.replace(/(.*)=/, '').replace(/"|'/g, '') 407 break 408 409 if !name 410 name = if isImg then "image" else if isAudio then "audio" else "attachment" 411 name += "_" + Math.floor(Math.random() * 100) 412 slashPos = mimeType.indexOf('/') 413 414 type = mimeType.substring(slashPos + 1) 415 416 if type.length < 4 417 name += "." + type 418 419 regex = /(.*)content-id:(.*)<(.*)>/i 420 attach = 421 type: mimeType 422 base64: body 423 name: name 424 cid: regex.exec(rawHeaders)?[3] 425 visible: /png|jpeg|jpg|gif/.test(mimeType) 426 427 readyMail.attaches.push attach 428 429 else if isHtml or isPlain 430 body = decodeBody body, rawHeaders 431 body = _util.decode(body, part.contentType) 432 readyMail.html += body if isHtml 433 readyMail.text += body if isPlain 434 435 else 436 console.log "Unknown mime type: #{mimeType}" 437 438 null 439 440 try 441 parts = rawMailObj.messageParts 442 if !parts 443 return readyMail 444 445 mimeType = (parts.mimeType || "").toLowerCase() 446 isText = /text\/plain/.test(mimeType) 447 isHtml = /text\/html/.test(mimeType) 448 449 if mimeType.indexOf('multipart') isnt -1 450 parseBodyParts parts.bodyParts 451 else if isText or isHtml 452 body = decodeBody parts.body, parts.rawHeaders 453 body = _util.decode body, parts.contentType 454 readyMail.html = body if isHtml 455 readyMail.text = body if isText 456 else 457 console.log "Warning: mime type isn't supported! mime=#{mimeType}" 458 459 catch err 460 throw new Error err 461 462 wrapPreTag = (txt) -> 463 "<pre>" + _util.toHtmlEntity(txt) + "</pre>" 464 465 mergeInnerMsgs = (mail) -> 466 innerMsgs = mail.innerMsgs 467 if innerMsgs?.length 468 if !_util.trim(mail.html) and mail.text 469 mail.html += wrapPreTag mail.text 470 471 for innerMsg in innerMsgs 472 msg = mergeInnerMsgs innerMsg 473 txt = msg.text 474 htm = msg.html 475 if htm 476 mail.html += htm 477 else if txt 478 mail.html += wrapPerTag txt 479 mail.text += txt 480 if msg.attaches?.length > 0 481 mail.attaches = mail.attaches.concat(msg.attaches) 482 483 mail 484 485 result = mergeInnerMsgs readyMail 486 result 487 488 toMimeObj = (mimeMsgText) -> 489 rawMailObj = MailParser mimeMsgText 490 mailObj = buildMimeObj rawMailObj 491 mailObj 492 493 { toMimeTxt, toMimeObj }