mime-js

Create MIME message from browser, fork of https://github.com/ikr0m/mime-js
git clone git://git.defalsify.org/mime-js.git
Log | Files | Refs | LICENSE

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, '&amp;').replace(/</g, '&lt;')
     54       .replace(/>/, '&gt;').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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    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 }