imap_folder_agent_spec.rb 12 KB

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