mail.scm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016 Sou Bunnbu <iyzsong@member.fsf.org>
  3. ;;; Copyright © 2017 Carlo Zancanaro <carlo@zancanaro.id.au>
  4. ;;; Copyright © 2017, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
  5. ;;; Copyright © 2018 Oleg Pykhalov <go.wigust@gmail.com>
  6. ;;; Copyright © 2018 Clément Lassieur <clement@lassieur.org>
  7. ;;; Copyright © 2019 Christopher Baines <mail@cbaines.net>
  8. ;;; Copyright © 2019, 2020 Tobias Geerinckx-Rice <me@tobias.gr>
  9. ;;;
  10. ;;; This file is part of GNU Guix.
  11. ;;;
  12. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  13. ;;; under the terms of the GNU General Public License as published by
  14. ;;; the Free Software Foundation; either version 3 of the License, or (at
  15. ;;; your option) any later version.
  16. ;;;
  17. ;;; GNU Guix is distributed in the hope that it will be useful, but
  18. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  19. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. ;;; GNU General Public License for more details.
  21. ;;;
  22. ;;; You should have received a copy of the GNU General Public License
  23. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  24. (define-module (gnu tests mail)
  25. #:use-module (gnu tests)
  26. #:use-module (gnu packages mail)
  27. #:use-module (gnu system)
  28. #:use-module (gnu system accounts)
  29. #:use-module (gnu system shadow)
  30. #:use-module (gnu system vm)
  31. #:use-module (gnu services)
  32. #:use-module (gnu services base)
  33. #:use-module (gnu services getmail)
  34. #:use-module (gnu services mail)
  35. #:use-module (gnu services networking)
  36. #:use-module (guix gexp)
  37. #:use-module (guix store)
  38. #:use-module (ice-9 ftw)
  39. #:export (%test-opensmtpd
  40. %test-exim
  41. %test-dovecot
  42. %test-getmail))
  43. (define %opensmtpd-os
  44. (simple-operating-system
  45. (service dhcp-client-service-type)
  46. (service opensmtpd-service-type
  47. (opensmtpd-configuration
  48. (config-file
  49. (plain-file "smtpd.conf" "
  50. listen on 0.0.0.0
  51. action inbound mbox
  52. match from any for local action inbound
  53. "))))))
  54. (define (run-opensmtpd-test)
  55. "Return a test of an OS running OpenSMTPD service."
  56. (define vm
  57. (virtual-machine
  58. (operating-system (marionette-operating-system
  59. %opensmtpd-os
  60. #:imported-modules '((gnu services herd))))
  61. (port-forwardings '((1025 . 25)))))
  62. (define test
  63. (with-imported-modules '((gnu build marionette))
  64. #~(begin
  65. (use-modules (rnrs base)
  66. (srfi srfi-64)
  67. (ice-9 rdelim)
  68. (ice-9 regex)
  69. (gnu build marionette))
  70. (define marionette
  71. (make-marionette '(#$vm)))
  72. (define (read-reply-code port)
  73. "Read a SMTP reply from PORT and return its reply code."
  74. (let* ((line (read-line port))
  75. (mo (string-match "([0-9]+)([ -]).*" line))
  76. (code (string->number (match:substring mo 1)))
  77. (finished? (string= " " (match:substring mo 2))))
  78. (if finished?
  79. code
  80. (read-reply-code port))))
  81. (test-runner-current (system-test-runner #$output))
  82. (test-begin "opensmptd")
  83. (test-assert "service is running"
  84. (marionette-eval
  85. '(begin
  86. (use-modules (gnu services herd))
  87. (start-service 'smtpd))
  88. marionette))
  89. (test-assert "mbox is empty"
  90. (marionette-eval
  91. '(and (file-exists? "/var/spool/mail")
  92. (not (file-exists? "/var/spool/mail/root")))
  93. marionette))
  94. (test-eq "accept an email"
  95. #t
  96. (let* ((smtp (socket AF_INET SOCK_STREAM 0))
  97. (addr (make-socket-address AF_INET INADDR_LOOPBACK 1025)))
  98. (connect smtp addr)
  99. ;; Be greeted.
  100. (read-reply-code smtp) ;220
  101. ;; Greet the server.
  102. (write-line "EHLO somehost" smtp)
  103. (read-reply-code smtp) ;250
  104. ;; Set sender email.
  105. (write-line "MAIL FROM: <someone>" smtp)
  106. (read-reply-code smtp) ;250
  107. ;; Set recipient email.
  108. (write-line "RCPT TO: <root>" smtp)
  109. (read-reply-code smtp) ;250
  110. ;; Send message.
  111. (write-line "DATA" smtp)
  112. (read-reply-code smtp) ;354
  113. (write-line "Subject: Hello" smtp)
  114. (newline smtp)
  115. (write-line "Nice to meet you!" smtp)
  116. (write-line "." smtp)
  117. (read-reply-code smtp) ;250
  118. ;; Say goodbye.
  119. (write-line "QUIT" smtp)
  120. (read-reply-code smtp) ;221
  121. (close smtp)
  122. #t))
  123. (test-assert "mail arrived"
  124. (marionette-eval
  125. '(begin
  126. (use-modules (ice-9 popen)
  127. (ice-9 rdelim))
  128. (define (queue-empty?)
  129. (let* ((pipe (open-pipe* OPEN_READ
  130. #$(file-append opensmtpd
  131. "/sbin/smtpctl")
  132. "show" "queue"))
  133. (line (read-line pipe)))
  134. (close-pipe pipe)
  135. (eof-object? line)))
  136. (let wait ((n 20))
  137. (cond ((queue-empty?)
  138. (file-exists? "/var/spool/mail/root"))
  139. ((zero? n)
  140. (error "root mailbox didn't show up"))
  141. (else
  142. (sleep 1) (wait (- n 1))))))
  143. marionette))
  144. (test-end))))
  145. (gexp->derivation "opensmtpd-test" test))
  146. (define %test-opensmtpd
  147. (system-test
  148. (name "opensmtpd")
  149. (description "Send an email to a running OpenSMTPD server.")
  150. (value (run-opensmtpd-test))))
  151. (define %exim-os
  152. (simple-operating-system
  153. (service dhcp-client-service-type)
  154. (service mail-aliases-service-type '())
  155. (service exim-service-type
  156. (exim-configuration
  157. (config-file
  158. (plain-file "exim.conf" "
  159. primary_hostname = komputilo
  160. domainlist local_domains = @
  161. domainlist relay_to_domains =
  162. hostlist relay_from_hosts = localhost
  163. never_users =
  164. acl_smtp_rcpt = acl_check_rcpt
  165. acl_smtp_data = acl_check_data
  166. begin acl
  167. acl_check_rcpt:
  168. accept
  169. acl_check_data:
  170. accept
  171. "))))))
  172. (define (run-exim-test)
  173. "Return a test of an OS running an Exim service."
  174. (define vm
  175. (virtual-machine
  176. (operating-system (marionette-operating-system
  177. %exim-os
  178. #:imported-modules '((gnu services herd))))
  179. (port-forwardings '((1025 . 25)))))
  180. (define test
  181. (with-imported-modules '((gnu build marionette))
  182. #~(begin
  183. (use-modules (rnrs base)
  184. (srfi srfi-64)
  185. (ice-9 ftw)
  186. (ice-9 rdelim)
  187. (ice-9 regex)
  188. (gnu build marionette))
  189. (define marionette
  190. (make-marionette '(#$vm)))
  191. (define (read-reply-code port)
  192. "Read a SMTP reply from PORT and return its reply code."
  193. (let* ((line (read-line port))
  194. (mo (string-match "([0-9]+)([ -]).*" line))
  195. (code (string->number (match:substring mo 1)))
  196. (finished? (string= " " (match:substring mo 2))))
  197. (if finished?
  198. code
  199. (read-reply-code port))))
  200. (define smtp (socket AF_INET SOCK_STREAM 0))
  201. (define addr (make-socket-address AF_INET INADDR_LOOPBACK 1025))
  202. (test-runner-current (system-test-runner #$output))
  203. (test-begin "exim")
  204. (test-assert "service is running"
  205. (marionette-eval
  206. '(begin
  207. (use-modules (gnu services herd))
  208. (start-service 'exim))
  209. marionette))
  210. (sleep 1) ;; give the service time to start talking
  211. (connect smtp addr)
  212. ;; Be greeted.
  213. (test-eq "greeting received"
  214. 220 (read-reply-code smtp))
  215. ;; Greet the server.
  216. (write-line "EHLO somehost" smtp)
  217. (test-eq "greeting successful"
  218. 250 (read-reply-code smtp))
  219. ;; Set sender email.
  220. (write-line "MAIL FROM: test@example.com" smtp)
  221. (test-eq "sender set"
  222. 250 (read-reply-code smtp)) ;250
  223. ;; Set recipient email.
  224. (write-line "RCPT TO: root@komputilo" smtp)
  225. (test-eq "recipient set"
  226. 250 (read-reply-code smtp)) ;250
  227. ;; Send message.
  228. (write-line "DATA" smtp)
  229. (test-eq "data begun"
  230. 354 (read-reply-code smtp)) ;354
  231. (write-line "Subject: Hello" smtp)
  232. (newline smtp)
  233. (write-line "Nice to meet you!" smtp)
  234. (write-line "." smtp)
  235. (test-eq "message sent"
  236. 250 (read-reply-code smtp)) ;250
  237. ;; Say goodbye.
  238. (write-line "QUIT" smtp)
  239. (test-eq "quit successful"
  240. 221 (read-reply-code smtp)) ;221
  241. (close smtp)
  242. (test-eq "the email is received"
  243. 1
  244. (marionette-eval
  245. '(begin
  246. (use-modules (ice-9 ftw))
  247. (length (scandir "/var/spool/exim/msglog"
  248. (lambda (x) (not (string-prefix? "." x))))))
  249. marionette))
  250. (test-end))))
  251. (gexp->derivation "exim-test" test))
  252. (define %test-exim
  253. (system-test
  254. (name "exim")
  255. (description "Send an email to a running an Exim server.")
  256. (value (run-exim-test))))
  257. (define %dovecot-os
  258. (simple-operating-system
  259. (service dhcp-client-service-type)
  260. (dovecot-service #:config
  261. (dovecot-configuration
  262. (disable-plaintext-auth? #f)
  263. (ssl? "no")
  264. (auth-mechanisms '("anonymous"))
  265. (auth-anonymous-username "alice")
  266. (mail-location
  267. (string-append "maildir:~/Maildir"
  268. ":INBOX=~/Maildir/INBOX"
  269. ":LAYOUT=fs"))))))
  270. (define (run-dovecot-test)
  271. "Return a test of an OS running Dovecot service."
  272. (define vm
  273. (virtual-machine
  274. (operating-system (marionette-operating-system
  275. %dovecot-os
  276. #:imported-modules '((gnu services herd))))
  277. (port-forwardings '((8143 . 143)))))
  278. (define test
  279. (with-imported-modules '((gnu build marionette))
  280. #~(begin
  281. (use-modules (gnu build marionette)
  282. (ice-9 iconv)
  283. (ice-9 rdelim)
  284. (rnrs base)
  285. (rnrs bytevectors)
  286. (srfi srfi-64))
  287. (define marionette
  288. (make-marionette '(#$vm)))
  289. (define* (message-length message #:key (encoding "iso-8859-1"))
  290. (bytevector-length (string->bytevector message encoding)))
  291. (define message "From: test@example.com\n\
  292. Subject: Hello Nice to meet you!")
  293. (test-runner-current (system-test-runner #$output))
  294. (test-begin "dovecot")
  295. ;; Wait for dovecot to be up and running.
  296. (test-assert "dovecot running"
  297. (marionette-eval
  298. '(begin
  299. (use-modules (gnu services herd))
  300. (start-service 'dovecot))
  301. marionette))
  302. ;; Check Dovecot service's PID.
  303. (test-assert "service process id"
  304. (let ((pid
  305. (number->string (wait-for-file "/var/run/dovecot/master.pid"
  306. marionette))))
  307. (marionette-eval `(file-exists? (string-append "/proc/" ,pid))
  308. marionette)))
  309. (test-assert "accept an email"
  310. (let ((imap (socket AF_INET SOCK_STREAM 0))
  311. (addr (make-socket-address AF_INET INADDR_LOOPBACK 8143)))
  312. (connect imap addr)
  313. ;; Be greeted.
  314. (read-line imap) ;OK
  315. ;; Authenticate
  316. (write-line "a AUTHENTICATE ANONYMOUS" imap)
  317. (read-line imap) ;+
  318. (write-line "c2lyaGM=" imap)
  319. (read-line imap) ;OK
  320. ;; Create a TESTBOX mailbox
  321. (write-line "a CREATE TESTBOX" imap)
  322. (read-line imap) ;OK
  323. ;; Append a message to a TESTBOX mailbox
  324. (write-line (format #f "a APPEND TESTBOX {~a}"
  325. (number->string (message-length message)))
  326. imap)
  327. (read-line imap) ;+
  328. (write-line message imap)
  329. (read-line imap) ;OK
  330. ;; Logout
  331. (write-line "a LOGOUT" imap)
  332. (close imap)
  333. #t))
  334. (test-equal "mail arrived"
  335. message
  336. (marionette-eval
  337. '(begin
  338. (use-modules (ice-9 ftw)
  339. (ice-9 match))
  340. (let ((TESTBOX/new "/home/alice/Maildir/TESTBOX/new/"))
  341. (match (scandir TESTBOX/new)
  342. (("." ".." message-file)
  343. (call-with-input-file
  344. (string-append TESTBOX/new message-file)
  345. get-string-all)))))
  346. marionette))
  347. (test-end))))
  348. (gexp->derivation "dovecot-test" test))
  349. (define %test-dovecot
  350. (system-test
  351. (name "dovecot")
  352. (description "Connect to a running Dovecot server.")
  353. (value (run-dovecot-test))))
  354. (define %getmail-os
  355. (operating-system
  356. (inherit (simple-operating-system))
  357. ;; Set a password for the user account; the test needs it.
  358. (users (cons (user-account
  359. (name "alice")
  360. (password (crypt "testpass" "$6$abc"))
  361. (comment "Bob's sister")
  362. (group "users")
  363. (supplementary-groups '("wheel" "audio" "video")))
  364. %base-user-accounts))
  365. (services (cons* (service dhcp-client-service-type)
  366. (service dovecot-service-type
  367. (dovecot-configuration
  368. (disable-plaintext-auth? #f)
  369. (ssl? "no")
  370. (auth-mechanisms '("anonymous" "plain"))
  371. (auth-anonymous-username "alice")
  372. (mail-location
  373. (string-append "maildir:~/Maildir"
  374. ":INBOX=~/Maildir/INBOX"
  375. ":LAYOUT=fs"))))
  376. (service getmail-service-type
  377. (list
  378. (getmail-configuration
  379. (name 'test)
  380. (user "alice")
  381. (directory "/var/lib/getmail/alice")
  382. (idle '("TESTBOX"))
  383. (rcfile
  384. (getmail-configuration-file
  385. (retriever
  386. (getmail-retriever-configuration
  387. (type "SimpleIMAPRetriever")
  388. (server "localhost")
  389. (username "alice")
  390. (port 143)
  391. (extra-parameters
  392. '((password . "testpass")
  393. (mailboxes . ("TESTBOX"))))))
  394. (destination
  395. (getmail-destination-configuration
  396. (type "Maildir")
  397. (path "/home/alice/TestMaildir/")))
  398. (options
  399. (getmail-options-configuration
  400. (read-all #f))))))))
  401. %base-services))))
  402. (define (run-getmail-test)
  403. "Return a test of an OS running Getmail service."
  404. (define vm
  405. (virtual-machine
  406. (operating-system (marionette-operating-system
  407. %getmail-os
  408. #:imported-modules '((gnu services herd))))
  409. (port-forwardings '((8143 . 143)))))
  410. (define test
  411. (with-imported-modules '((gnu build marionette))
  412. #~(begin
  413. (use-modules (gnu build marionette)
  414. (ice-9 iconv)
  415. (ice-9 rdelim)
  416. (rnrs base)
  417. (rnrs bytevectors)
  418. (srfi srfi-64))
  419. (define marionette
  420. (make-marionette '(#$vm)))
  421. (define* (message-length message #:key (encoding "iso-8859-1"))
  422. (bytevector-length (string->bytevector message encoding)))
  423. (define message "From: test@example.com\n\
  424. Subject: Hello Nice to meet you!")
  425. (test-runner-current (system-test-runner #$output))
  426. (test-begin "getmail")
  427. ;; Wait for dovecot to be up and running.
  428. (test-assert "dovecot running"
  429. (marionette-eval
  430. '(begin
  431. (use-modules (gnu services herd))
  432. (start-service 'dovecot))
  433. marionette))
  434. ;; Wait for getmail to be up and running.
  435. (test-assert "getmail-test running"
  436. (marionette-eval
  437. '(let* ((pw (getpw "alice"))
  438. (uid (passwd:uid pw))
  439. (gid (passwd:gid pw)))
  440. (use-modules (gnu services herd))
  441. (for-each
  442. (lambda (dir)
  443. (mkdir dir)
  444. (chown dir uid gid))
  445. '("/home/alice/TestMaildir"
  446. "/home/alice/TestMaildir/cur"
  447. "/home/alice/TestMaildir/new"
  448. "/home/alice/TestMaildir/tmp"
  449. "/home/alice/TestMaildir/TESTBOX"
  450. "/home/alice/TestMaildir/TESTBOX/cur"
  451. "/home/alice/TestMaildir/TESTBOX/new"
  452. "/home/alice/TestMaildir/TESTBOX/tmp"))
  453. (start-service 'getmail-test))
  454. marionette))
  455. ;; Check Dovecot service's PID.
  456. (test-assert "service process id"
  457. (let ((pid
  458. (number->string (wait-for-file "/var/run/dovecot/master.pid"
  459. marionette))))
  460. (marionette-eval `(file-exists? (string-append "/proc/" ,pid))
  461. marionette)))
  462. (test-assert "accept an email"
  463. (let ((imap (socket AF_INET SOCK_STREAM 0))
  464. (addr (make-socket-address AF_INET INADDR_LOOPBACK 8143)))
  465. (connect imap addr)
  466. ;; Be greeted.
  467. (read-line imap) ;OK
  468. ;; Authenticate
  469. (write-line "a AUTHENTICATE ANONYMOUS" imap)
  470. (read-line imap) ;+
  471. (write-line "c2lyaGM=" imap)
  472. (read-line imap) ;OK
  473. ;; Create a TESTBOX mailbox
  474. (write-line "a CREATE TESTBOX" imap)
  475. (read-line imap) ;OK
  476. ;; Append a message to a TESTBOX mailbox
  477. (write-line (format #f "a APPEND TESTBOX {~a}"
  478. (number->string (message-length message)))
  479. imap)
  480. (read-line imap) ;+
  481. (write-line message imap)
  482. (read-line imap) ;OK
  483. ;; Logout
  484. (write-line "a LOGOUT" imap)
  485. (close imap)
  486. #t))
  487. (sleep 1)
  488. (test-assert "mail arrived"
  489. (string-contains
  490. (marionette-eval
  491. '(begin
  492. (use-modules (ice-9 ftw)
  493. (ice-9 match))
  494. (let ((TESTBOX/new "/home/alice/TestMaildir/new/"))
  495. (match (scandir TESTBOX/new)
  496. (("." ".." message-file)
  497. (call-with-input-file
  498. (string-append TESTBOX/new message-file)
  499. get-string-all)))))
  500. marionette)
  501. message))
  502. (test-end))))
  503. (gexp->derivation "getmail-test" test))
  504. (define %test-getmail
  505. (system-test
  506. (name "getmail")
  507. (description "Connect to a running Getmail server.")
  508. (value (run-getmail-test))))