Class: Whisper::EmailReceiver

Inherits:
Object
  • Object
show all
Includes:
Loggy
Defined in:
lib/whisper/email_receiver.rb

Constant Summary collapse

DELAY =
20

Instance Method Summary collapse

Constructor Details

#initialize(entryset, commentset, email_sender, authors, mbox_fn, offset_fn, comment_dir) ⇒ EmailReceiver

Returns a new instance of EmailReceiver.



16
17
18
19
20
21
22
23
24
25
26
# File 'lib/whisper/email_receiver.rb', line 16

def initialize entryset, commentset, email_sender, authors, mbox_fn, offset_fn, comment_dir
  @entryset = entryset
  @commentset = commentset
  @email_sender = email_sender
  @offset_fn = offset_fn
  @authors = authors
  @comment_dir = comment_dir
  offset = IO.read(offset_fn).to_i rescue 0
  @mbox_fn = mbox_fn
  @mbox = Mbox.new mbox_fn, offset
end

Instance Method Details

#done?Boolean

Returns:

  • (Boolean)


45
# File 'lib/whisper/email_receiver.rb', line 45

def done?; @mbox.eof? end

#offset=(offset) ⇒ Object



28
29
30
# File 'lib/whisper/email_receiver.rb', line 28

def offset= offset
  @mbox = Mbox.new @mbox_fn, offset
end

#process_one_messageObject



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
138
139
140
141
142
143
144
# File 'lib/whisper/email_receiver.rb', line 91

def process_one_message
  offset = @mbox.offset
  body, headers = @mbox.next_message
  return unless body

  message_id, reply_to, from, date = %w(message-id in-reply-to from date).map do |f|
    headers[f] or begin
      warn "no #{f} for message at offset #{offset}. have headers: #{headers.keys.inspect}"
      return
    end
  end

  if message_id =~ /^<(.+?)>$/
    message_id = $1
  end

  if reply_to =~ /^<(.+?)>$/
    reply_to = $1
  end

  @commentset.refresh! # force comment set to refresh itself before we look for parents
  entry, parent_comment = case reply_to
  when /whisper-post-(\S+)@/
    unless(entry = @entryset.entries_by_id[$1])
      warn "invalid entry id in message-id for message at offset #{offset}: #{$1.inspect}"
      return
    end
    debug "found new message from #{headers['from']} on entry #{entry.id}"
    [entry, nil]
  else
    unless(parent_comment = @commentset.comments_by_message_id[reply_to])
      warn "no such comment id in in-reply-to for message at offset #{offset}: #{reply_to.inspect}"
      return
    end
    unless(entry = @entryset.entries_by_id[parent_comment.entry_id])
      warn "comment #{comment.id} doesn't reference a real entry? wtf (offset #{offset})"
      return
    end
    debug "found new message from #{headers['from']} on entry #{entry.id} replying to a comment by #{parent_comment.author}"
    [entry, parent_comment]
  end

  ## pull out variables and comments
  author_vars = {}
  content, author_vars = pull_out_content_and_vars body
  @authors[from.email_address] = author_vars

  vars = { :author => from, :offset => offset, :entry_id => entry.id, :message_id => message_id,
           :resend_setting => (author_vars[Comment::RESEND_SETTING] || @authors[from.email_address][Comment::RESEND_SETTING]),
           :url_setting => (author_vars[Comment::URL_SETTING] || @authors[from.email_address][Comment::RESEND_SETTING]),
           :published => Time.parse(date), :parent_id => (parent_comment ? parent_comment.id : nil) }

  [content, vars, entry, parent_comment]
end

#pull_out_content_and_vars(body) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/whisper/email_receiver.rb', line 166

def pull_out_content_and_vars body
  vars = {}
  last_was_comment = false
  attribution = nil

  lines = body.gsub(/^-- \n.*\Z/m, "").lines.to_a
  content = lines.zip(lines[1 .. -1] || []).map do |l, next_l|
    case l
    when /^[^>].*:$/
      if next_l =~ /^> ;/
        ## top-level attribution followed by comments. may need to consume it.
        # puts "have attribution: #{l}"
        attribution = l
        nil
      else
        l
      end
    when /^> ! (.*?):\s*(.*?)(\s*;.*)?$/
      vars[$1] = $2
      nil
    when /^> ;/
      last_was_comment = true
      nil
    when /^>\s*$/
      last_was_comment ? nil : l
    when /^> /
      if attribution
        both = [attribution, l]
        attribution = nil
        both
      else
        l
      end
    else
      l
    end
  end.flatten.compact.join
  [content, vars]
end

#resend_comments(comment, parent_comment, entry) ⇒ Object

resend a new comment to everyone who should get it



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
# File 'lib/whisper/email_receiver.rb', line 60

def resend_comments comment, parent_comment, entry
  sent_to = Set.new [comment.author.email_address] # never send the comment back to the original author

  if parent_comment
    addr = parent_comment.author.email_address
    resend = parent_comment.resend_setting
    debug "for parent comment, resend for #{addr} is #{resend.inspect}"
    if resend == "replies-only"
      info "scheduling a resend of #{comment.id} to #{addr} because they're the author of the parent comment and resend is #{resend}"
      sent_to << addr
      @email_sender.send_comment comment, addr
    end
  else
    ## resend to entry author if it's a top-level comment
    info "scheduling a resend of top-level comment #{comment.id} to entry author #{entry.author.email_address}"
    @email_sender.send_comment comment, entry.author.email_address
  end

  (@commentset.comments_by_entry_id[entry.id] || []).each do |c| # iterate over all comments for the entry
    addr = c.author.email_address
    next if sent_to.member? addr # we've already seen him
    sent_to << addr
    resend = c.resend_setting
    debug "for thread, resend for #{addr} comment #{c.id} is #{resend.inspect}"
    if resend == "all"
      info "scheduling a resend of #{comment.id} to #{addr} because they're in the thread and resend is #{resend}"
      @email_sender.send_comment comment, addr
    end
  end
end

#start!Object



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/whisper/email_receiver.rb', line 32

def start!
  Thread.new do
    while true
      begin
        sleep DELAY
        step
      rescue Exception => e
        warn ["(#{e.class}) #{e.message}", e.backtrace].flatten.join("\n")
      end
    end
  end
end

#stepObject



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/whisper/email_receiver.rb', line 47

def step
  return false if @mbox.eof?

  content, vars, entry, parent_comment = process_one_message
  if content
    comment = write_one_message content, vars
    resend_comments comment, parent_comment, entry
  end
  update_offset!
  true
end

#update_offset!Object



206
207
208
# File 'lib/whisper/email_receiver.rb', line 206

def update_offset!
  File.open(@offset_fn, "w") { |f| f.puts @mbox.offset }
end

#write_one_message(content, vars) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/whisper/email_receiver.rb', line 146

def write_one_message content, vars
  id = Digest::MD5.hexdigest [vars[:author], vars[:entry_id], vars[:message_id]].join("|")

  dir = File.join @comment_dir, vars[:entry_id]
  Dir.mkdir dir unless File.exist? dir

  name = File.join dir, id
  yaml_fn = name + ENTRY_METADATA_EXTENSION
  warn "overwriting comment yaml file: #{yaml_fn}" if File.exist? yaml_fn
  File.open(yaml_fn, "w") { |f| f.puts vars.merge({ :id => id }).to_yaml }

  textile_fn = name + ENTRY_CONTENT_EXTENSION
  warn "overwriting comment textile file: #{textile_fn}" if File.exist? textile_fn
  File.open(textile_fn, "w") { |f| f.print content }

  info "wrote comment #{yaml_fn}"

  Comment.new CachedFile.new(yaml_fn), CachedFile.new(textile_fn)
end