mail.scm 21 KB

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