1
0

imap_folder_agent_spec.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. require 'rails_helper'
  2. require 'time'
  3. describe Agents::ImapFolderAgent do
  4. module MessageMixin
  5. def folder
  6. 'INBOX'
  7. end
  8. def uidvalidity
  9. 100
  10. end
  11. def has_attachment?
  12. false
  13. end
  14. def body_parts(mime_types = %(text/plain text/enriched text/html))
  15. mime_types.map do |type|
  16. all_parts.find do |part|
  17. part.mime_type == type
  18. end
  19. end.compact.map! do |part|
  20. part.extend(Agents::ImapFolderAgent::Message::Scrubbed)
  21. end
  22. end
  23. def uid; end
  24. def raw_mail; end
  25. def delete; end
  26. def mark_as_read; end
  27. include Agents::ImapFolderAgent::Message::Scrubbed
  28. end
  29. describe 'checking IMAP' do
  30. let(:valid_options) do
  31. {
  32. 'expected_update_period_in_days' => 1,
  33. 'host' => 'mail.example.net',
  34. 'ssl' => true,
  35. 'username' => 'foo',
  36. 'password' => 'bar',
  37. 'folders' => ['INBOX'],
  38. 'conditions' => {}
  39. }
  40. end
  41. let(:mails) do
  42. [
  43. Mail.read(Rails.root.join('spec/data_fixtures/imap1.eml')).tap do |mail|
  44. mail.extend(MessageMixin)
  45. allow(mail).to receive(:uid).and_return(1)
  46. allow(mail).to receive(:raw_mail).and_return(mail.encoded)
  47. end,
  48. Mail.read(Rails.root.join('spec/data_fixtures/imap2.eml')).tap do |mail|
  49. mail.extend(MessageMixin)
  50. allow(mail).to receive(:uid).and_return(2)
  51. allow(mail).to receive(:has_attachment?).and_return(true)
  52. allow(mail).to receive(:raw_mail).and_return(mail.encoded)
  53. end
  54. ]
  55. end
  56. let(:expected_payloads) do
  57. [
  58. {
  59. 'message_id' => 'foo.123@mail.example.jp',
  60. 'folder' => 'INBOX',
  61. 'from' => 'nanashi.gombeh@example.jp',
  62. 'to' => ['jane.doe@example.com', 'john.doe@example.com'],
  63. 'cc' => [],
  64. 'date' => '2014-05-09T16:00:00+09:00',
  65. 'subject' => 'some subject',
  66. 'body' => "Some plain text\nSome second line\n",
  67. 'has_attachment' => false,
  68. 'matches' => {},
  69. 'mime_type' => 'text/plain'
  70. },
  71. {
  72. 'message_id' => 'bar.456@mail.example.com',
  73. 'folder' => 'INBOX',
  74. 'from' => 'john.doe@example.com',
  75. 'to' => ['jane.doe@example.com', 'nanashi.gombeh@example.jp'],
  76. 'cc' => [],
  77. 'subject' => 'Re: some subject',
  78. 'body' => "Some reply\n",
  79. 'date' => '2014-05-09T17:00:00+09:00',
  80. 'has_attachment' => true,
  81. 'matches' => {},
  82. 'mime_type' => 'text/plain'
  83. }
  84. ]
  85. end
  86. before do
  87. @checker = Agents::ImapFolderAgent.new(name: 'Example', options: valid_options, keep_events_for: 2.days)
  88. @checker.user = users(:bob)
  89. @checker.save!
  90. allow(@checker).to receive(:each_unread_mail) { |&yielder|
  91. seen = @checker.lastseen
  92. notified = @checker.notified
  93. mails.each do |mail|
  94. yielder[mail, notified]
  95. seen[mail.uidvalidity] = mail.uid
  96. end
  97. @checker.lastseen = seen
  98. @checker.notified = notified
  99. nil
  100. }
  101. end
  102. describe 'validations' do
  103. before do
  104. expect(@checker).to be_valid
  105. end
  106. it 'should validate the integer fields' do
  107. @checker.options['expected_update_period_in_days'] = 'nonsense'
  108. expect(@checker).not_to be_valid
  109. @checker.options['expected_update_period_in_days'] = '2'
  110. expect(@checker).to be_valid
  111. @checker.options['port'] = -1
  112. expect(@checker).not_to be_valid
  113. @checker.options['port'] = 'imap'
  114. expect(@checker).not_to be_valid
  115. @checker.options['port'] = '143'
  116. expect(@checker).to be_valid
  117. @checker.options['port'] = 993
  118. expect(@checker).to be_valid
  119. end
  120. it 'should validate the boolean fields' do
  121. %w[ssl mark_as_read].each do |key|
  122. @checker.options[key] = 1
  123. expect(@checker).not_to be_valid
  124. @checker.options[key] = false
  125. expect(@checker).to be_valid
  126. @checker.options[key] = 'true'
  127. expect(@checker).to be_valid
  128. @checker.options[key] = ''
  129. expect(@checker).to be_valid
  130. end
  131. end
  132. it 'should validate regexp conditions' do
  133. @checker.options['conditions'] = {
  134. 'subject' => '(foo'
  135. }
  136. expect(@checker).not_to be_valid
  137. @checker.options['conditions'] = {
  138. 'body' => '***'
  139. }
  140. expect(@checker).not_to be_valid
  141. @checker.options['conditions'] = {
  142. 'subject' => '\ARe:',
  143. 'body' => '(?<foo>http://\S+)'
  144. }
  145. expect(@checker).to be_valid
  146. end
  147. end
  148. describe '#check' do
  149. it 'should check for mails and save memory' do
  150. expect { @checker.check }.to change { Event.count }.by(2)
  151. expect(@checker.notified.sort).to eq(mails.map(&:message_id).sort)
  152. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  153. seen[mail.uidvalidity] = mail.uid
  154. end)
  155. expect(Event.last(2).map(&:payload)).to eq expected_payloads
  156. expect { @checker.check }.not_to(change { Event.count })
  157. end
  158. it 'should narrow mails by To' do
  159. @checker.options['conditions']['to'] = 'John.Doe@*'
  160. expect { @checker.check }.to change { Event.count }.by(1)
  161. expect(@checker.notified.sort).to eq([mails.first.message_id])
  162. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  163. seen[mail.uidvalidity] = mail.uid
  164. end)
  165. expect(Event.last.payload).to eq(expected_payloads.first)
  166. expect { @checker.check }.not_to(change { Event.count })
  167. end
  168. it 'should not fail when a condition on Cc is given and a mail does not have the field' do
  169. @checker.options['conditions']['cc'] = 'John.Doe@*'
  170. expect do
  171. expect { @checker.check }.not_to(change { Event.count })
  172. end.not_to raise_exception
  173. end
  174. it 'should perform regexp matching and save named captures' do
  175. @checker.options['conditions'].update(
  176. 'subject' => '\ARe: (?<a>.+)',
  177. 'body' => 'Some (?<b>.+) reply'
  178. )
  179. expect { @checker.check }.to change { Event.count }.by(1)
  180. expect(@checker.notified.sort).to eq([mails.last.message_id])
  181. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  182. seen[mail.uidvalidity] = mail.uid
  183. end)
  184. expect(Event.last.payload).to eq(expected_payloads.last.update(
  185. 'body' => "<div dir=\"ltr\">Some HTML reply<br></div>\n",
  186. 'matches' => { 'a' => 'some subject', 'b' => 'HTML' },
  187. 'mime_type' => 'text/html'
  188. ))
  189. expect { @checker.check }.not_to(change { Event.count })
  190. end
  191. it 'should narrow mails by has_attachment (true)' do
  192. @checker.options['conditions']['has_attachment'] = true
  193. expect { @checker.check }.to change { Event.count }.by(1)
  194. expect(Event.last.payload['subject']).to eq('Re: some subject')
  195. end
  196. it 'should narrow mails by has_attachment (false)' do
  197. @checker.options['conditions']['has_attachment'] = false
  198. expect { @checker.check }.to change { Event.count }.by(1)
  199. expect(Event.last.payload['subject']).to eq('some subject')
  200. end
  201. it 'should narrow mail parts by MIME types' do
  202. @checker.options['mime_types'] = %w[text/plain]
  203. @checker.options['conditions'].update(
  204. 'subject' => '\ARe: (?<a>.+)',
  205. 'body' => 'Some (?<b>.+) reply'
  206. )
  207. expect { @checker.check }.not_to(change { Event.count })
  208. expect(@checker.notified.sort).to eq([])
  209. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  210. seen[mail.uidvalidity] = mail.uid
  211. end)
  212. end
  213. it 'should never mark mails as read unless mark_as_read is true' do
  214. mails.each do |mail|
  215. allow(mail).to receive(:mark_as_read).never
  216. end
  217. expect { @checker.check }.to change { Event.count }.by(2)
  218. end
  219. it 'should mark mails as read if mark_as_read is true' do
  220. @checker.options['mark_as_read'] = true
  221. mails.each do |mail|
  222. allow(mail).to receive(:mark_as_read).once
  223. end
  224. expect { @checker.check }.to change { Event.count }.by(2)
  225. end
  226. it 'should create just one event for multiple mails with the same Message-Id' do
  227. mails.first.message_id = mails.last.message_id
  228. @checker.options['mark_as_read'] = true
  229. mails.each do |mail|
  230. allow(mail).to receive(:mark_as_read).once
  231. end
  232. expect { @checker.check }.to change { Event.count }.by(1)
  233. end
  234. it 'should delete mails if delete is true' do
  235. @checker.options['delete'] = true
  236. mails.each do |mail|
  237. allow(mail).to receive(:delete).once
  238. end
  239. expect { @checker.check }.to change { Event.count }.by(2)
  240. end
  241. describe 'processing mails with a broken From header value' do
  242. before do
  243. # "from" patterns work against mail addresses and not
  244. # against text parts, so these mails should be skipped if a
  245. # "from" condition is given.
  246. #
  247. # Mail::Header#[]= does not accept an invalid value, so set it directly
  248. mails.first.header.fields.replace_field Mail::Field.new('from', '.')
  249. mails.last.header.fields.replace_field Mail::Field.new('from', '@')
  250. end
  251. it 'should ignore them without failing if a "from" condition is given' do
  252. @checker.options['conditions']['from'] = '*'
  253. expect { @checker.check }.not_to(change { Event.count })
  254. end
  255. end
  256. describe 'with event_headers' do
  257. let(:expected_headers) do
  258. [
  259. {
  260. 'mime_version' => '1.0',
  261. 'x_foo' => "test1-1\ntest1-2"
  262. },
  263. {
  264. 'mime_version' => '1.0',
  265. 'x_foo' => "test2-1\ntest2-2"
  266. }
  267. ]
  268. end
  269. before do
  270. expected_payloads.zip(expected_headers) do |payload, headers|
  271. payload['headers'] = headers
  272. end
  273. @checker.options['event_headers'] = %w[mime-version x-foo]
  274. @checker.options['event_headers_style'] = 'snakecased'
  275. @checker.save!
  276. end
  277. it 'should check for mails and emit events with headers' do
  278. expect { @checker.check }.to change { Event.count }.by(2)
  279. expect(@checker.notified.sort).to eq(mails.map(&:message_id).sort)
  280. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  281. seen[mail.uidvalidity] = mail.uid
  282. end)
  283. expect(Event.last(2).map(&:payload)).to match expected_payloads
  284. expect { @checker.check }.not_to(change { Event.count })
  285. end
  286. end
  287. describe 'with include_raw_mail' do
  288. before do
  289. @checker.options['include_raw_mail'] = true
  290. @checker.save!
  291. end
  292. it 'should check for mails and emit events with raw_mail' do
  293. expect { @checker.check }.to change { Event.count }.by(2)
  294. expect(@checker.notified.sort).to eq(mails.map(&:message_id).sort)
  295. expect(@checker.lastseen).to eq(mails.each_with_object(@checker.make_seen) do |mail, seen|
  296. seen[mail.uidvalidity] = mail.uid
  297. end)
  298. expect(Event.last(2).map(&:payload)).to match(expected_payloads.map.with_index do |payload, i|
  299. payload.merge(
  300. 'raw_mail' => satisfy { |d| Base64.decode64(d) == mails[i].encoded }
  301. )
  302. end)
  303. expect { @checker.check }.not_to(change { Event.count })
  304. end
  305. end
  306. end
  307. end
  308. describe 'Agents::ImapFolderAgent::Message::Scrubbed' do
  309. before do
  310. @class = Class.new do
  311. def subject
  312. "broken\xB7subject\xB6"
  313. end
  314. def body
  315. "broken\xB7body\xB6"
  316. end
  317. include Agents::ImapFolderAgent::Message::Scrubbed
  318. end
  319. @object = @class.new
  320. end
  321. describe '#scrubbed' do
  322. it 'should return a scrubbed string' do
  323. expect(@object.scrubbed(:subject)).to eq('broken<b7>subject<b6>')
  324. expect(@object.scrubbed(:body)).to eq('broken<b7>body<b6>')
  325. end
  326. end
  327. end
  328. end