module Sisimai::Lhost::Office365

Sisimai::Lhost::Office365 parses a bounce email which created by Microsoft Office 365. Methods in the module are called from only Sisimai::Message.

Constants

Headers365
Indicators
MarkingsOf
ReBackbone
ReCommands
StartingOf
StatusList

Public Class Methods

description() click to toggle source
# File lib/sisimai/lhost/office365.rb, line 212
def description; return 'Microsoft Office 365: https://office.microsoft.com/'; end
make(mhead, mbody) click to toggle source

Parse bounce messages from Microsoft Office 365 @param [Hash] mhead Message headers of a bounce email @param [String] mbody Message body of a bounce email @return [Hash] Bounce data list and message/rfc822 part @return [Nil] it failed to parse or the arguments are missing

# File lib/sisimai/lhost/office365.rb, line 78
def make(mhead, mbody)
  # X-MS-Exchange-Message-Is-Ndr:
  # X-Microsoft-Antispam-PRVS: <....@...outlook.com>
  # X-Exchange-Antispam-Report-Test: UriScan:;
  # X-Exchange-Antispam-Report-CFA-Test:
  # X-MS-Exchange-CrossTenant-OriginalArrivalTime: 29 Apr 2015 23:34:45.6789 (JST)
  # X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted
  # X-MS-Exchange-Transport-CrossTenantHeadersStamped: ...
  tryto  = %r/.+[.](?:outbound[.]protection|prod)[.]outlook[.]com\b/
  match  = 0
  match += 1 if mhead['subject'].include?('Undeliverable:')
  Headers365.each do |e|
    next if mhead[e].nil?
    next if mhead[e].empty?
    match += 1
  end
  match += 1 if mhead['received'].any? { |a| a =~ tryto }
  if mhead['message-id']
    # Message-ID: <00000000-0000-0000-0000-000000000000@*.*.prod.outlook.com>
    match += 1 if mhead['message-id'] =~ tryto
  end
  return nil if match < 2

  require 'sisimai/rfc1894'
  fieldtable = Sisimai::RFC1894.FIELDTABLE
  permessage = {}     # (Hash) Store values of each Per-Message field
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]
  emailsteak = Sisimai::RFC5322.fillet(mbody, ReBackbone)
  bodyslices = emailsteak[0].split("\n")
  readcursor = 0      # (Integer) Points the current cursor position
  recipients = 0      # (Integer) The number of 'Final-Recipient' header
  connheader = {}
  endoferror = false  # (Boolean) Flag for the end of error messages
  htmlbegins = false  # (Boolean) Flag for HTML part
  v = nil

  while e = bodyslices.shift do
    # Read error messages and delivery status lines from the head of the email
    # to the previous line of the beginning of the original message.
    if readcursor == 0
      # Beginning of the bounce message or delivery status part
      readcursor |= Indicators[:deliverystatus] if e =~ MarkingsOf[:message]
      next
    end
    next if (readcursor & Indicators[:deliverystatus]) == 0
    next if e.empty?

    # kijitora@example.com<mailto:kijitora@example.com>
    # The email address wasn't found at the destination domain. It might
    # be misspelled or it might not exist any longer. Try retyping the
    # address and resending the message.
    #
    # Original Message Details
    # Created Date:   4/29/2017 6:40:30 AM
    # Sender Address: neko@example.jp
    # Recipient Address:      kijitora@example.org
    # Subject:        Nyaan#
    v = dscontents[-1]

    if cv = e.match(/\A.+[@].+[<]mailto:(.+[@].+)[>]\z/) ||
            e.match(/\ARecipient[ ]Address:[ ]+(.+)\z/)
      # kijitora@example.com<mailto:kijitora@example.com>
      if v['recipient']
        # There are multiple recipient addresses in the message body.
        dscontents << Sisimai::Lhost.DELIVERYSTATUS
        v = dscontents[-1]
      end
      v['recipient'] = cv[1]
      recipients += 1

    elsif cv = e.match(/\AGenerating server: (.+)\z/)
      # Generating server: FFFFFFFFFFFF.e0.prod.outlook.com
      connheader['lhost'] = cv[1].downcase
    else
      if endoferror
        # After "Original message headers:"
        next unless f = Sisimai::RFC1894.match(e)
        next unless o = Sisimai::RFC1894.field(e)
        next unless fieldtable[o[0]]
        next if o[0] =~ /\A(?:diagnostic-code|final-recipient)\z/
        v[fieldtable[o[0]]] = o[2]

        next unless f == 1
        permessage[fieldtable[o[0]]] = o[2]
      else
        if e =~ MarkingsOf[:error]
          # Diagnostic information for administrators:
          v['diagnosis'] = e
        else
          # kijitora@example.com
          # Remote Server returned '550 5.1.10 RESOLVER.ADR.RecipientNotFound; Recipien=
          # t not found by SMTP address lookup'
          next unless v['diagnosis']
          if e =~ MarkingsOf[:eoe]
            # Original message headers:
            endoferror = true
            next
          end
          v['diagnosis'] << ' ' << e
        end
      end
    end
  end
  return nil unless recipients > 0

  dscontents.each do |e|
    # Set default values if each value is empty.
    permessage.each_key { |a| e[a] ||= permessage[a] || '' }

    e['status']  ||= ''
    e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) || ''
    if e['status'].empty? || e['status'].end_with?('.0.0')
      # There is no value of Status header or the value is 5.0.0, 4.0.0
      e['status'] = Sisimai::SMTP::Status.find(e['diagnosis']) || e['status']
    end

    ReCommands.each_key do |p|
      # Try to match with regular expressions defined in ReCommands
      next unless e['diagnosis'] =~ ReCommands[p]
      e['command'] = p.to_s
      break
    end
    next unless e['status']

    StatusList.each_key do |f|
      # Try to match with each key as a regular expression
      next unless e['status'] =~ f
      e['reason'] = StatusList[f]
      break
    end
  end

  return { 'ds' => dscontents, 'rfc822' => emailsteak[1] }
end