mail.scm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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 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. (mkdir #$output)
  82. (chdir #$output)
  83. (test-begin "opensmptd")
  84. (test-assert "service is running"
  85. (marionette-eval
  86. '(begin
  87. (use-modules (gnu services herd))
  88. (start-service 'smtpd))
  89. marionette))
  90. (test-assert "mbox is empty"
  91. (marionette-eval
  92. '(and (file-exists? "/var/spool/mail")
  93. (not (file-exists? "/var/spool/mail/root")))
  94. marionette))
  95. (test-eq "accept an email"
  96. #t
  97. (let* ((smtp (socket AF_INET SOCK_STREAM 0))
  98. (addr (make-socket-address AF_INET INADDR_LOOPBACK 1025)))
  99. (connect smtp addr)
  100. ;; Be greeted.
  101. (read-reply-code smtp) ;220
  102. ;; Greet the server.
  103. (write-line "EHLO somehost" smtp)
  104. (read-reply-code smtp) ;250
  105. ;; Set sender email.
  106. (write-line "MAIL FROM: <someone>" smtp)
  107. (read-reply-code smtp) ;250
  108. ;; Set recipient email.
  109. (write-line "RCPT TO: <root>" smtp)
  110. (read-reply-code smtp) ;250
  111. ;; Send message.
  112. (write-line "DATA" smtp)
  113. (read-reply-code smtp) ;354
  114. (write-line "Subject: Hello" smtp)
  115. (newline smtp)
  116. (write-line "Nice to meet you!" smtp)
  117. (write-line "." smtp)
  118. (read-reply-code smtp) ;250
  119. ;; Say goodbye.
  120. (write-line "QUIT" smtp)
  121. (read-reply-code smtp) ;221
  122. (close smtp)
  123. #t))
  124. (test-assert "mail arrived"
  125. (marionette-eval
  126. '(begin
  127. (use-modules (ice-9 popen)
  128. (ice-9 rdelim))
  129. (define (queue-empty?)
  130. (let* ((pipe (open-pipe* OPEN_READ
  131. #$(file-append opensmtpd
  132. "/sbin/smtpctl")
  133. "show" "queue"))
  134. (line (read-line pipe)))
  135. (close-pipe pipe)
  136. (eof-object? line)))
  137. (let wait ((n 20))
  138. (cond ((queue-empty?)
  139. (file-exists? "/var/spool/mail/root"))
  140. ((zero? n)
  141. (error "root mailbox didn't show up"))
  142. (else
  143. (sleep 1) (wait (- n 1))))))
  144. marionette))
  145. (test-end)
  146. (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
  147. (gexp->derivation "opensmtpd-test" test))
  148. (define %test-opensmtpd
  149. (system-test
  150. (name "opensmtpd")
  151. (description "Send an email to a running OpenSMTPD server.")
  152. (value (run-opensmtpd-test))))
  153. (define %exim-os
  154. (simple-operating-system
  155. (service dhcp-client-service-type)
  156. (service mail-aliases-service-type '())
  157. (service exim-service-type
  158. (exim-configuration
  159. (config-file
  160. (plain-file "exim.conf" "
  161. primary_hostname = komputilo
  162. domainlist local_domains = @
  163. domainlist relay_to_domains =
  164. hostlist relay_from_hosts = localhost
  165. never_users =
  166. acl_smtp_rcpt = acl_check_rcpt
  167. acl_smtp_data = acl_check_data
  168. begin acl
  169. acl_check_rcpt:
  170. accept
  171. acl_check_data:
  172. accept
  173. "))))))
  174. (define (run-exim-test)
  175. "Return a test of an OS running an Exim service."
  176. (define vm
  177. (virtual-machine
  178. (operating-system (marionette-operating-system
  179. %exim-os
  180. #:imported-modules '((gnu services herd))))
  181. (port-forwardings '((1025 . 25)))))
  182. (define test
  183. (with-imported-modules '((gnu build marionette)
  184. (ice-9 ftw))
  185. #~(begin
  186. (use-modules (rnrs base)
  187. (srfi srfi-64)
  188. (ice-9 ftw)
  189. (ice-9 rdelim)
  190. (ice-9 regex)
  191. (gnu build marionette))
  192. (define marionette
  193. (make-marionette '(#$vm)))
  194. (define (read-reply-code port)
  195. "Read a SMTP reply from PORT and return its reply code."
  196. (let* ((line (read-line port))
  197. (mo (string-match "([0-9]+)([ -]).*" line))
  198. (code (string->number (match:substring mo 1)))
  199. (finished? (string= " " (match:substring mo 2))))
  200. (if finished?
  201. code
  202. (read-reply-code port))))
  203. (define smtp (socket AF_INET SOCK_STREAM 0))
  204. (define addr (make-socket-address AF_INET INADDR_LOOPBACK 1025))
  205. (mkdir #$output)
  206. (chdir #$output)
  207. (test-begin "exim")
  208. (test-assert "service is running"
  209. (marionette-eval
  210. '(begin
  211. (use-modules (gnu services herd))
  212. (start-service 'exim))
  213. marionette))
  214. (sleep 1) ;; give the service time to start talking
  215. (connect smtp addr)
  216. ;; Be greeted.
  217. (test-eq "greeting received"
  218. 220 (read-reply-code smtp))
  219. ;; Greet the server.
  220. (write-line "EHLO somehost" smtp)
  221. (test-eq "greeting successful"
  222. 250 (read-reply-code smtp))
  223. ;; Set sender email.
  224. (write-line "MAIL FROM: test@example.com" smtp)
  225. (test-eq "sender set"
  226. 250 (read-reply-code smtp)) ;250
  227. ;; Set recipient email.
  228. (write-line "RCPT TO: root@komputilo" smtp)
  229. (test-eq "recipient set"
  230. 250 (read-reply-code smtp)) ;250
  231. ;; Send message.
  232. (write-line "DATA" smtp)
  233. (test-eq "data begun"
  234. 354 (read-reply-code smtp)) ;354
  235. (write-line "Subject: Hello" smtp)
  236. (newline smtp)
  237. (write-line "Nice to meet you!" smtp)
  238. (write-line "." smtp)
  239. (test-eq "message sent"
  240. 250 (read-reply-code smtp)) ;250
  241. ;; Say goodbye.
  242. (write-line "QUIT" smtp)
  243. (test-eq "quit successful"
  244. 221 (read-reply-code smtp)) ;221
  245. (close smtp)
  246. (test-eq "the email is received"
  247. 1
  248. (marionette-eval
  249. '(begin
  250. (use-modules (ice-9 ftw))
  251. (length (scandir "/var/spool/exim/msglog"
  252. (lambda (x) (not (string-prefix? "." x))))))
  253. marionette))
  254. (test-end)
  255. (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
  256. (gexp->derivation "exim-test" test))
  257. (define %test-exim
  258. (system-test
  259. (name "exim")
  260. (description "Send an email to a running an Exim server.")
  261. (value (run-exim-test))))
  262. (define %dovecot-os
  263. (simple-operating-system
  264. (service dhcp-client-service-type)
  265. (dovecot-service #:config
  266. (dovecot-configuration
  267. (disable-plaintext-auth? #f)
  268. (ssl? "no")
  269. (auth-mechanisms '("anonymous"))
  270. (auth-anonymous-username "alice")
  271. (mail-location
  272. (string-append "maildir:~/Maildir"
  273. ":INBOX=~/Maildir/INBOX"
  274. ":LAYOUT=fs"))))))
  275. (define (run-dovecot-test)
  276. "Return a test of an OS running Dovecot service."
  277. (define vm
  278. (virtual-machine
  279. (operating-system (marionette-operating-system
  280. %dovecot-os
  281. #:imported-modules '((gnu services herd))))
  282. (port-forwardings '((8143 . 143)))))
  283. (define test
  284. (with-imported-modules '((gnu build marionette))
  285. #~(begin
  286. (use-modules (gnu build marionette)
  287. (ice-9 iconv)
  288. (ice-9 rdelim)
  289. (rnrs base)
  290. (rnrs bytevectors)
  291. (srfi srfi-64))
  292. (define marionette
  293. (make-marionette '(#$vm)))
  294. (define* (message-length message #:key (encoding "iso-8859-1"))
  295. (bytevector-length (string->bytevector message encoding)))
  296. (define message "From: test@example.com\n\
  297. Subject: Hello Nice to meet you!")
  298. (mkdir #$output)
  299. (chdir #$output)
  300. (test-begin "dovecot")
  301. ;; Wait for dovecot to be up and running.
  302. (test-assert "dovecot running"
  303. (marionette-eval
  304. '(begin
  305. (use-modules (gnu services herd))
  306. (start-service 'dovecot))
  307. marionette))
  308. ;; Check Dovecot service's PID.
  309. (test-assert "service process id"
  310. (let ((pid
  311. (number->string (wait-for-file "/var/run/dovecot/master.pid"
  312. marionette))))
  313. (marionette-eval `(file-exists? (string-append "/proc/" ,pid))
  314. marionette)))
  315. (test-assert "accept an email"
  316. (let ((imap (socket AF_INET SOCK_STREAM 0))
  317. (addr (make-socket-address AF_INET INADDR_LOOPBACK 8143)))
  318. (connect imap addr)
  319. ;; Be greeted.
  320. (read-line imap) ;OK
  321. ;; Authenticate
  322. (write-line "a AUTHENTICATE ANONYMOUS" imap)
  323. (read-line imap) ;+
  324. (write-line "c2lyaGM=" imap)
  325. (read-line imap) ;OK
  326. ;; Create a TESTBOX mailbox
  327. (write-line "a CREATE TESTBOX" imap)
  328. (read-line imap) ;OK
  329. ;; Append a message to a TESTBOX mailbox
  330. (write-line (format #f "a APPEND TESTBOX {~a}"
  331. (number->string (message-length message)))
  332. imap)
  333. (read-line imap) ;+
  334. (write-line message imap)
  335. (read-line imap) ;OK
  336. ;; Logout
  337. (write-line "a LOGOUT" imap)
  338. (close imap)
  339. #t))
  340. (test-equal "mail arrived"
  341. message
  342. (marionette-eval
  343. '(begin
  344. (use-modules (ice-9 ftw)
  345. (ice-9 match))
  346. (let ((TESTBOX/new "/home/alice/Maildir/TESTBOX/new/"))
  347. (match (scandir TESTBOX/new)
  348. (("." ".." message-file)
  349. (call-with-input-file
  350. (string-append TESTBOX/new message-file)
  351. get-string-all)))))
  352. marionette))
  353. (test-end)
  354. (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
  355. (gexp->derivation "dovecot-test" test))
  356. (define %test-dovecot
  357. (system-test
  358. (name "dovecot")
  359. (description "Connect to a running Dovecot server.")
  360. (value (run-dovecot-test))))
  361. (define %getmail-os
  362. (operating-system
  363. (inherit (simple-operating-system))
  364. ;; Set a password for the user account; the test needs it.
  365. (users (cons (user-account
  366. (name "alice")
  367. (password (crypt "testpass" "$6$abc"))
  368. (comment "Bob's sister")
  369. (group "users")
  370. (supplementary-groups '("wheel" "audio" "video")))
  371. %base-user-accounts))
  372. (services (cons* (service dhcp-client-service-type)
  373. (service dovecot-service-type
  374. (dovecot-configuration
  375. (disable-plaintext-auth? #f)
  376. (ssl? "no")
  377. (auth-mechanisms '("anonymous" "plain"))
  378. (auth-anonymous-username "alice")
  379. (mail-location
  380. (string-append "maildir:~/Maildir"
  381. ":INBOX=~/Maildir/INBOX"
  382. ":LAYOUT=fs"))))
  383. (service getmail-service-type
  384. (list
  385. (getmail-configuration
  386. (name 'test)
  387. (user "alice")
  388. (directory "/var/lib/getmail/alice")
  389. (idle '("TESTBOX"))
  390. (rcfile
  391. (getmail-configuration-file
  392. (retriever
  393. (getmail-retriever-configuration
  394. (type "SimpleIMAPRetriever")
  395. (server "localhost")
  396. (username "alice")
  397. (port 143)
  398. (extra-parameters
  399. '((password . "testpass")
  400. (mailboxes . ("TESTBOX"))))))
  401. (destination
  402. (getmail-destination-configuration
  403. (type "Maildir")
  404. (path "/home/alice/TestMaildir/")))
  405. (options
  406. (getmail-options-configuration
  407. (read-all #f))))))))
  408. %base-services))))
  409. (define (run-getmail-test)
  410. "Return a test of an OS running Getmail service."
  411. (define vm
  412. (virtual-machine
  413. (operating-system (marionette-operating-system
  414. %getmail-os
  415. #:imported-modules '((gnu services herd))))
  416. (port-forwardings '((8143 . 143)))))
  417. (define test
  418. (with-imported-modules '((gnu build marionette))
  419. #~(begin
  420. (use-modules (gnu build marionette)
  421. (ice-9 iconv)
  422. (ice-9 rdelim)
  423. (rnrs base)
  424. (rnrs bytevectors)
  425. (srfi srfi-64))
  426. (define marionette
  427. (make-marionette '(#$vm)))
  428. (define* (message-length message #:key (encoding "iso-8859-1"))
  429. (bytevector-length (string->bytevector message encoding)))
  430. (define message "From: test@example.com\n\
  431. Subject: Hello Nice to meet you!")
  432. (mkdir #$output)
  433. (chdir #$output)
  434. (test-begin "getmail")
  435. ;; Wait for dovecot to be up and running.
  436. (test-assert "dovecot running"
  437. (marionette-eval
  438. '(begin
  439. (use-modules (gnu services herd))
  440. (start-service 'dovecot))
  441. marionette))
  442. ;; Wait for getmail to be up and running.
  443. (test-assert "getmail-test running"
  444. (marionette-eval
  445. '(let* ((pw (getpw "alice"))
  446. (uid (passwd:uid pw))
  447. (gid (passwd:gid pw)))
  448. (use-modules (gnu services herd))
  449. (for-each
  450. (lambda (dir)
  451. (mkdir dir)
  452. (chown dir uid gid))
  453. '("/home/alice/TestMaildir"
  454. "/home/alice/TestMaildir/cur"
  455. "/home/alice/TestMaildir/new"
  456. "/home/alice/TestMaildir/tmp"
  457. "/home/alice/TestMaildir/TESTBOX"
  458. "/home/alice/TestMaildir/TESTBOX/cur"
  459. "/home/alice/TestMaildir/TESTBOX/new"
  460. "/home/alice/TestMaildir/TESTBOX/tmp"))
  461. (start-service 'getmail-test))
  462. marionette))
  463. ;; Check Dovecot service's PID.
  464. (test-assert "service process id"
  465. (let ((pid
  466. (number->string (wait-for-file "/var/run/dovecot/master.pid"
  467. marionette))))
  468. (marionette-eval `(file-exists? (string-append "/proc/" ,pid))
  469. marionette)))
  470. (test-assert "accept an email"
  471. (let ((imap (socket AF_INET SOCK_STREAM 0))
  472. (addr (make-socket-address AF_INET INADDR_LOOPBACK 8143)))
  473. (connect imap addr)
  474. ;; Be greeted.
  475. (read-line imap) ;OK
  476. ;; Authenticate
  477. (write-line "a AUTHENTICATE ANONYMOUS" imap)
  478. (read-line imap) ;+
  479. (write-line "c2lyaGM=" imap)
  480. (read-line imap) ;OK
  481. ;; Create a TESTBOX mailbox
  482. (write-line "a CREATE TESTBOX" imap)
  483. (read-line imap) ;OK
  484. ;; Append a message to a TESTBOX mailbox
  485. (write-line (format #f "a APPEND TESTBOX {~a}"
  486. (number->string (message-length message)))
  487. imap)
  488. (read-line imap) ;+
  489. (write-line message imap)
  490. (read-line imap) ;OK
  491. ;; Logout
  492. (write-line "a LOGOUT" imap)
  493. (close imap)
  494. #t))
  495. (sleep 1)
  496. (test-assert "mail arrived"
  497. (string-contains
  498. (marionette-eval
  499. '(begin
  500. (use-modules (ice-9 ftw)
  501. (ice-9 match))
  502. (let ((TESTBOX/new "/home/alice/TestMaildir/new/"))
  503. (match (scandir TESTBOX/new)
  504. (("." ".." message-file)
  505. (call-with-input-file
  506. (string-append TESTBOX/new message-file)
  507. get-string-all)))))
  508. marionette)
  509. message))
  510. (test-end)
  511. (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
  512. (gexp->derivation "getmail-test" test))
  513. (define %test-getmail
  514. (system-test
  515. (name "getmail")
  516. (description "Connect to a running Getmail server.")
  517. (value (run-getmail-test))))