Module: Sisimai::RFC3834

Defined in:
lib/sisimai/rfc3834.rb

Overview

Sisimai::RFC3834 - RFC3834 auto reply message detector

Constant Summary collapse

MarkingsOf =
{:boundary => '__SISIMAI_PSEUDO_BOUNDARY__'}
LowerLabel =
%w[from to subject auto-submitted precedence x-apple-action].freeze
DoNotParse =
{
  'from'    => ['root@', 'postmaster@', 'mailer-daemon@'],
  'to'      => ['root@'],
  'subject' => [
      'security information for', # sudo(1)
      'mail failure -',           # Exim
  ],
}.freeze
AutoReply0 =
{
  # http://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml
  'auto-submitted' => ['auto-generated', 'auto-replied', 'auto-notified'],
  'precedence'     => ['auto_reply'],
  'subject'        => ['auto:', 'auto response:', 'automatic reply:', 'out of office:', 'out of the office:'],
  'x-apple-action' => ['vacation'],
}.freeze
SubjectSet =
%r{\A(?>
   (?:.+?)?re:
  |auto(?:[ ]response):
  |automatic[ ]reply:
  |out[ ]of[ ]office:
  )
  [ ]*(.+)\z
}x.freeze
Suspending =
[
  ["this email inbox", " is no longer in use."],
].freeze

Class Method Summary collapse

Class Method Details

.descriptionObject



138
# File 'lib/sisimai/rfc3834.rb', line 138

def description; 'Detector for auto replied message'; end

.inquire(mhead, mbody) ⇒ Hash, Nil

Detect auto reply message as RFC3834

Parameters:

  • mhead (Hash)

    Message headers of a bounce email

  • mbody (String)

    Message body of a bounce email

Returns:

  • (Hash)

    Bounce data list and message/rfc822 part

  • (Nil)

    it failed to decode or the arguments are missing



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/sisimai/rfc3834.rb', line 40

def inquire(mhead, mbody)
  leave = 0
  match = 0
  lower = {} 

  LowerLabel.each do |e|
    # Set lower-cased value of each header related to auto-response
    next if mhead.has_key?(e) == false
    lower[e] = mhead[e].downcase
  end

  # DETECT_EXCLUSION_MESSAGE
  DoNotParse.each_key do |e|
    # Exclude message from root@
    next if lower[e].nil? || DoNotParse[e].none? { |a| lower[e].include?(a) }
    leave = 1
    break
  end
  return nil if leave > 0

  # DETECT_AUTO_REPLY_MESSAGE0
  AutoReply0.each_key do |e|
    # RFC3834 Auto-Submitted and other headers
    next if lower[e].nil? || AutoReply0[e].none? { |a| lower[e].include?(a) }
    match += 1
    break
  end
  return nil if match < 1

  require 'sisimai/lhost'
  dscontents = [Sisimai::Lhost.DELIVERYSTATUS]; v = dscontents[-1]
  bodyslices = mbody.scrub('?').split("\n")
  rfc822part = '' # (String) message/rfc822-headers part
  recipients = 0  # (Integer) The number of 'Final-Recipient' header
  maxmsgline = 5  # (Integer) Max message length(lines)
  haveloaded = 0  # (Integer) The number of lines loaded from message body
  blanklines = 0  # (Integer) Counter for countinuous blank lines
  countuntil = 1  # (Integer) Maximum value of blank lines in the body part

  # RECIPIENT_ADDRESS
  %w[from return-path].each do |e|
    # Try to get the address of the recipient
    next if mhead[e].nil?
    v['recipient'] = mhead[e]
    break
  end

  if v["recipient"] != ""
    # Clean-up the recipient address
    v['recipient'] = Sisimai::Address.s3s4(v['recipient'])
    recipients += 1
  end
  return nil if recipients == 0

  if mhead['content-type']
    # Get the boundary string and set regular expression for matching with the boundary string.
    q = Sisimai::RFC2045.boundary(mhead['content-type'], 0) || ''
    MarkingsOf[:boundary] = q if q.empty? == false
  end

  # MESSAGE_BODY: Get the vacation message
  while e = bodyslices.shift do
    # Read the first 5 lines except a blank line
    countuntil += 1 if e.include?(MarkingsOf[:boundary])

    if e.empty?
      # Check a blank line
      blanklines += 1
      break if blanklines > countuntil
      next
    end
    next if !e.include?(' ') || e.start_with?('Content-Type', 'Content-Transfer')

    v['diagnosis'] ||= ''
    v['diagnosis']  += "#{e }"
    haveloaded += 1
    break if haveloaded >= maxmsgline
  end
  v['diagnosis'] ||= mhead['subject']
  v['reason']      = 'vacation'

  cv = v['diagnosis'].downcase
  Suspending.each do |e|
    # Check that the auto-replied message indicates the "Suspend" reason or not.
    next unless Sisimai::String.aligned(cv, e)
    v['reason'] = 'suspend'
    break
  end

  v['date']   = mhead['date']
  v['status'] = ''

  if cv = lower['subject'].match(SubjectSet)
    # Get the Subject header from the original message
    rfc822part = "Subject: #{cv[1]}\n"
  end
  return { 'ds' => dscontents, 'rfc822' => rfc822part }
end