From e8c929f3c8db09547eb3dd76d7e320ad1b8a9f25 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 15 Apr 2025 16:03:59 +0200 Subject: [PATCH 01/11] Minor improvements to the sendmail suite --- rosetta-test-suites/sendmail.ros | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index dfe39b1..5c8fba4 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -67,6 +67,9 @@ (define (server-message-contains? content) (string-contains? (server-message-data server) content)) + (define (server-message-contains-ci? content) + (string-contains-ci? (server-message-data server) content)) + (setup (lambda () (set! server '()))) @@ -84,7 +87,7 @@ (setup (lambda () (set! server (start-mock-server)) (server-set-extensions! server '("STARTTLS")) - (server-set-auths! server '(("LOGIN" ("account" "password")))) + (server-set-auths! server '(("LOGIN" ("account" "password")))) (define (login-auth-proc credentials connection success failure) (let ((login-user-response @@ -280,11 +283,7 @@ (test "set header with unicode value" (lambda () (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table '(("x-my-header" "¡some value")))) (assert - (or - (server-message-contains? "x-my-header: =?utf-8?q?=C2=A1some?= value") - (server-message-contains? "x-my-header: =?utf-8?b?wqFzb21lIHZhbHVl?=") - (server-message-contains? "x-my-header: =?UTF-8?Q?=C2=A1some?= value") - (server-message-contains? "x-my-header: =?UTF-8?B?wqFzb21lIHZhbHVl?=")) + (server-message-contains-ci? "x-my-header: =?utf-8?q?=C2=A1some?= value") (string-append "Expected x-my-header to contain encoded ¡, but instead got " (server-message-data server))) (assert (server-message-contains? "message content") From 0c919157eeff746db462f1292fc83e21f025a287 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 15 Apr 2025 16:29:35 +0200 Subject: [PATCH 02/11] Adds first tests for sending attachments --- rosetta-test-suites/sendmail.ros | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index 5c8fba4..92b1c1c 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -28,14 +28,14 @@ (placeholder '(sendmail-connected? connection) "Should return true if the connection is still open") ; Send Mail - (placeholder '(sendmail-send-message-full connection message-content from to-list cc-list bcc-list headers message-options to-list-options) "Send a message to the server. The cc-list and bcc-list are optional. The headers hash is optional. The message-options and the to-list-options are optional. The to-list-options is a list of option tuple lists, one for each receiver. The function should return a list corresponding to the responses from the server for each recipient in the to-list. If there is only a single response, a list with a single response should be returned.") + (placeholder '(sendmail-send-message-full connection message-content from to-list cc-list bcc-list headers attachments message-options to-list-options) "Send a message to the server. The cc-list and bcc-list are optional. The headers hash is optional. The message-options and the to-list-options are optional. The to-list-options is a list of option tuple lists, one for each receiver. The function should return a list corresponding to the responses from the server for each recipient in the to-list. If there is only a single response, a list with a single response should be returned.") (define (sendmail-send-message-with-options connection message-content from to-list message-options to-list-options) - (sendmail-send-message-full connection message-content from to-list '() '() (make-hash-table) message-options to-list-options)) + (sendmail-send-message-full connection message-content from to-list '() '() (make-hash-table) (make-hash-table) message-options to-list-options)) (define (sendmail-send-message connection content from to-list) (sendmail-send-message-with-options connection content from to-list '() '())) (define (sendmail-send-message-with-headers connection content from to-list headers-hash-map) - (sendmail-send-message-full connection content from to-list '() '() headers-hash-map '() '())) + (sendmail-send-message-full connection content from to-list '() '() headers-hash-map (make-hash-table) '() '())) ; Response accessors (placeholder '(send-success? response) "Return whether the sending was successful.") @@ -181,7 +181,7 @@ (test "Send a message to cc recipients" (lambda () (let - ((responses (sendmail-send-message-full smtp-connection "message content" "sender@sender.to" '() '("user1@recipient.to") '() (make-hash-table) '() '()))) + ((responses (sendmail-send-message-full smtp-connection "message content" "sender@sender.to" '() '("user1@recipient.to") '() (make-hash-table) (make-hash-table) '() '()))) (assert (all? send-success? responses)) (assert (server-message-contains? "user1@recipient.to")) (assert @@ -193,7 +193,7 @@ (test "Send a message to bcc recipients" (lambda () (let - ((responses (sendmail-send-message-full smtp-connection "message content" "sender@sender.to" '() '() '("user1@recipient.to") (make-hash-table) '() '()))) + ((responses (sendmail-send-message-full smtp-connection "message content" "sender@sender.to" '() '() '("user1@recipient.to") (make-hash-table) (make-hash-table) '() '()))) (assert (all? send-success? responses)) (assert (not (server-message-contains? "user1@recipient.to"))) (assert @@ -336,6 +336,74 @@ )) + (capability 'attachments (list + + (define smtp-connection '()) + (define (connect-smtp-server) + (set! smtp-connection (sendmail-connect "localhost" (server-port server)))) + + (setup (lambda () + (set! server (start-mock-server)) + (connect-smtp-server))) + + (tearDown (lambda () + (sendmail-disconnect smtp-connection) + (set! smtp-connection '()))) + + ; attachments-properties is a list of alists with the following keys: + ; "data", "file-name", "content-type", or "content-disposition" + (define sendmail-send-with-attachments (lambda attachments-properties + (sendmail-send-message-full + smtp-connection + "message content" "sender@sender.to" '("recipient@recipient.to") + '() '() + (make-hash-table) + (map alist->hash-table attachments-properties) + '() '()))) + + (test "basic text attachment" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("file-name" "info.txt") + ("content-type" "text/plain"))) + (assert + (or + (server-message-contains? "content of info.txt") + (server-message-contains? "Y29udGVudCBvZiBpbmZvLnR4dA==")) + (string-append "Expected server to receive message with text attachment, but received: " (server-message-data server))) + (assert + (server-message-contains-ci? "content-disposition: attachment") + (string-append "Expected server to receive message with content-disposition: attachment, but received: " (server-message-data server))))) + + (test "basic image attachment" (lambda () + (sendmail-send-with-attachments + '(("data" "test.png") + ("file-name" "test.png") + ("content-type" "image/png"))) + (assert + ; only part of base64 encoded image + (server-message-contains? "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAAXNSR0IArs4c6QAAAARnQU1BAACx") + (string-append "Expected server to receive message with image attachment, but received: " (server-message-data server))) + (assert + (server-message-contains-ci? "content-disposition: attachment") + (string-append "Expected server to receive message with content-disposition: attachment, but received: " (server-message-data server))))) + + ; - Multiple attachments with the same name + ; - Attachment without a name + ; - inline vs attachment content-disposition + ; - inline attachment with ?cid? + ; - content/type multipart/mixed? + + (capability 'automatic-mime-detection (list + + )) + + (capability 'unicode-file-name (list + + )) + + )) + (capability 'general-crlf-injection (list (define smtp-connection '()) From d93ce2ed04639c91319591c2719ef63debb62c9a Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 15 Apr 2025 16:30:14 +0200 Subject: [PATCH 03/11] Adds fixtures for sendmail attachment tests --- rosetta-test-suites/sendmail-fixtures/info.txt | 1 + rosetta-test-suites/sendmail-fixtures/test.png | Bin 0 -> 128 bytes 2 files changed, 1 insertion(+) create mode 100644 rosetta-test-suites/sendmail-fixtures/info.txt create mode 100644 rosetta-test-suites/sendmail-fixtures/test.png diff --git a/rosetta-test-suites/sendmail-fixtures/info.txt b/rosetta-test-suites/sendmail-fixtures/info.txt new file mode 100644 index 0000000..0ecd895 --- /dev/null +++ b/rosetta-test-suites/sendmail-fixtures/info.txt @@ -0,0 +1 @@ +content of info.txt \ No newline at end of file diff --git a/rosetta-test-suites/sendmail-fixtures/test.png b/rosetta-test-suites/sendmail-fixtures/test.png new file mode 100644 index 0000000000000000000000000000000000000000..dec35f952790742dae2e377b282f18f3e1fd8ceb GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBL7~QRScvUi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctipf@f`+X#^d=bQh Date: Tue, 15 Apr 2025 16:30:53 +0200 Subject: [PATCH 04/11] [Py] Implements new send message placeholder with attachments --- rosetta-test-py/sendmail-python-emails.py | 19 ++++++++++++++++++- rosetta-test-py/sendmail-redmail.py | 12 ++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/rosetta-test-py/sendmail-python-emails.py b/rosetta-test-py/sendmail-python-emails.py index 3b0421c..6fbc671 100644 --- a/rosetta-test-py/sendmail-python-emails.py +++ b/rosetta-test-py/sendmail-python-emails.py @@ -121,7 +121,7 @@ def sendmail_connected(env, backend): # @sendmail_suite.placeholder("sendmail-send-message-full") -def sendmail_send_message(env, sender:SMTPBackend, message, sender_address, recipient_addresses, cc_addresses, bcc_addresses, custom_headers, message_options, recipients_options): +def sendmail_send_message(env, sender:SMTPBackend, message, sender_address, recipient_addresses, cc_addresses, bcc_addresses, custom_headers, attachments, message_options, recipients_options): try: message = emails.Message(text=message, subject="Test", @@ -130,6 +130,23 @@ def sendmail_send_message(env, sender:SMTPBackend, message, sender_address, reci cc=cc_addresses, bcc=bcc_addresses, headers=custom_headers,) + for attachment in attachments: + message_properties = { + "content_disposition": attachment.get("content-disposition", "attachment"), + } + + if "file-name" in attachment: + message_properties["filename"] = attachment["file-name"] + + read_mode = "r" + if attachment["data"].endswith(".png"): + read_mode = "rb" + message_properties["data"] = open(rosetta.fixture_path('sendmail-fixtures/' + attachment["data"]), read_mode) + + if "content-type" in attachment: + message_properties["content_type"] = attachment["content-type"] + + message.attach(**message_properties) return [message.send(smtp=sender,smtp_mail_options=message_options, smtp_rcpt_options=recipients_options)] except Exception as e: return [e] diff --git a/rosetta-test-py/sendmail-redmail.py b/rosetta-test-py/sendmail-redmail.py index 38917da..71dad40 100644 --- a/rosetta-test-py/sendmail-redmail.py +++ b/rosetta-test-py/sendmail-redmail.py @@ -2,6 +2,7 @@ import socket as socketlib import ssl from redmail import EmailSender +import pathlib import smtplib sendmail_suite = rosetta.suite("rosetta-test-suites/sendmail.ros") @@ -131,15 +132,20 @@ def sendmail_connected(env, sender: EmailSender): # @sendmail_suite.placeholder("sendmail-send-message-full") -def sendmail_send_message(env, sender: EmailSender, message, sender_address, recipient_addresses, cc_addresses, bcc_addresses, custom_headers, message_options, recipients_options): +def sendmail_send_message(env, sender: EmailSender, message, sender_address, recipient_addresses, cc_addresses, bcc_addresses, custom_headers, attachments, message_options, recipients_options): try: + redmail_attachments = { + attachment["file-name"]: pathlib.Path(rosetta.fixture_path('sendmail-fixtures/' + attachment["data"])) + for attachment in attachments + if attachment.get("content-disposition", "attachment") == "attachment"} sender.send(sender=sender_address, receivers=recipient_addresses, cc=cc_addresses, bcc=bcc_addresses, headers=custom_headers, subject="test", - text=message) + text=message, + attachments=redmail_attachments) except Exception as e: return [e] return [True] # EmailSender.send does not return anything related to the sending @@ -162,6 +168,8 @@ def sendmail_error(env, result): # Running # +#sendmail_suite.run(only=("test_basic_image_attachment",)) + sendmail_suite.run( exclude=( "test_CRLF_detection_in_send-message_recipient", From edcc7822f581c2dc2578b76b5a097b0fb377bf2f Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 15 Apr 2025 16:35:16 +0200 Subject: [PATCH 05/11] Fixes string-contains-ci? --- rosetta-test/stdlib.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosetta-test/stdlib.scm b/rosetta-test/stdlib.scm index 924eef1..ed61a77 100644 --- a/rosetta-test/stdlib.scm +++ b/rosetta-test/stdlib.scm @@ -289,5 +289,5 @@ (if (string-index str substr) #t #f)) (define (string-contains-ci? str substr) - (if (string-index-ci (string-downcase str) (string-downcase substr)) #t #f)) + (if (string-index (string-downcase str) (string-downcase substr)) #t #f)) ) \ No newline at end of file From e55f835aaae861dddeb70b6feee9071250b60c2c Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 12:26:22 +0200 Subject: [PATCH 06/11] Adds substring and string-contains-every to stdlib --- rosetta-test/interpreter-tests.json | 10 ++++++++- rosetta-test/stdlib.scm | 33 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/rosetta-test/interpreter-tests.json b/rosetta-test/interpreter-tests.json index 0daa483..87e7f9f 100644 --- a/rosetta-test/interpreter-tests.json +++ b/rosetta-test/interpreter-tests.json @@ -116,5 +116,13 @@ {"input": "(begin (hash-table-ref! ht 'c 3) (hash-table-ref ht 'c))", "expected": 3}, {"input": "(hash-table->alist ht)", "expected": [["b", 2], ["c", 3]]}, {"input": "(hash-table->alist (make-hash-table))", "expected": []}, - {"input": "(hash-table-ref (alist->hash-table '((a 10) (b 20))) 'b)", "expected": 20} + {"input": "(hash-table-ref (alist->hash-table '((a 10) (b 20))) 'b)", "expected": 20}, + {"input": "(substring \"abc\" 0 1)", "expected": "a"}, + {"input": "(substring \"abc\" 0 2)", "expected": "ab"}, + {"input": "(substring \"abc\" 0 3)", "expected": "abc"}, + {"input": "(substring \"abc\" 1 3)", "expected": "bc"}, + {"input": "(substring \"abc\" 0 0)", "expected": ""}, + {"input": "(substring \"abc\" 2 2)", "expected": ""}, + {"input": "(substring \"abc\" 3 4)", "expected": {"type": "Error"}}, + {"input": "(string-contains-every? \"abvabab\" \"ab\")", "expected": [0,3,5]} ] \ No newline at end of file diff --git a/rosetta-test/stdlib.scm b/rosetta-test/stdlib.scm index ed61a77..ab5ff17 100644 --- a/rosetta-test/stdlib.scm +++ b/rosetta-test/stdlib.scm @@ -285,9 +285,42 @@ (string-index-help (cdr str) substr (+ index 1))))) (string-index-help str substr 0)) + (define (substring str start end) + (if (or (< start 0) (< end 0) (> start (length str)) (> end (length str))) + (throw (error "substring: out of bounds"))) + (if (= start end) + "" + (begin + (define (substring-help-to-end str pos end res) + (if (or (empty? str) (= pos end)) + res + (substring-help-to-end (cdr str) (+ pos 1) end (string-append res (car str))))) + (define (substring-help-from-start str start pos) + (if (= start pos) + str + (substring-help-from-start (cdr str) start (+ pos 1)))) + (substring-help-to-end + (substring-help-from-start str start 0) + 0 end "")))) + (define (string-contains? str substr) (if (string-index str substr) #t #f)) (define (string-contains-ci? str substr) (if (string-index (string-downcase str) (string-downcase substr)) #t #f)) + + (define (string-contains-every? str substr) + (define result '()) + (define (string-index-help str substr index) + (if (empty? str) + result + (begin + (if (string-prefix? substr str) + (set! result (append result (list index)))) + (string-index-help (cdr str) substr (+ index 1))))) + (string-index-help str substr 0)) + + (define (string-contains-every-ci? str substr) + (string-contains-every? (string-downcase str) (string-downcase substr))) + ) \ No newline at end of file From e310412b5abf5808d07737432290cd1445726e80 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 12:26:47 +0200 Subject: [PATCH 07/11] Adds new attachment tests to sendmail suite --- .../sendmail-fixtures/info.txt | 2 +- rosetta-test-suites/sendmail.ros | 101 ++++++++++++++---- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/rosetta-test-suites/sendmail-fixtures/info.txt b/rosetta-test-suites/sendmail-fixtures/info.txt index 0ecd895..de24153 100644 --- a/rosetta-test-suites/sendmail-fixtures/info.txt +++ b/rosetta-test-suites/sendmail-fixtures/info.txt @@ -1 +1 @@ -content of info.txt \ No newline at end of file +content of info file \ No newline at end of file diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index 92b1c1c..e55d1af 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -5,11 +5,10 @@ ; indepdenent of whether this is done with an API from the tested library directly or by using ; another library, e.g. a MIME library from the standard library. (sources - '("CPython SMTP tests" "Python Foundation" "https://github.com/python/cpython/blob/9ba2a4638d7b620c939face7642b2f53a9fadc4b/Lib/test/test_smtplib.py") - '("Ruby net-smtp tests" "MRI maintainers" "https://github.com/ruby/net-smtp/blob/master/test/net/smtp/test_smtp.rb") + '("Ruby mail gem spec" "Mikel Lindsaar" "https://github.com/mikel/mail") '("SMTP RFC 2821" "IETF" "https://tools.ietf.org/html/rfc2821") '("SMTP RFC 5321" "IETF" "https://tools.ietf.org/html/rfc5321") - '("SMTP RFC 4954" "IETF" "https://tools.ietf.org/html/rfc4954")) + '("RFC 2183 - Content-Disposition" "IETF" "https://tools.ietf.org/html/rfc2183")) (list ; Socket @@ -353,28 +352,47 @@ ; attachments-properties is a list of alists with the following keys: ; "data", "file-name", "content-type", or "content-disposition" (define sendmail-send-with-attachments (lambda attachments-properties - (sendmail-send-message-full - smtp-connection - "message content" "sender@sender.to" '("recipient@recipient.to") - '() '() - (make-hash-table) - (map alist->hash-table attachments-properties) - '() '()))) + (let + ((properties-hashs (map alist->hash-table attachments-properties))) + (for-each + (lambda (property-hash) + (if (not (hash-table-exists? property-hash "content-disposition")) + (hash-table-set! property-hash "content-disposition" "attachment"))) + properties-hashs) + (sendmail-send-message-full + smtp-connection + "message content" "sender@sender.to" '("recipient@recipient.to") + '() '() + (make-hash-table) + properties-hashs + '() '())))) - (test "basic text attachment" (lambda () + (test "basic single text attachment" (lambda () (sendmail-send-with-attachments '(("data" "info.txt") ("file-name" "info.txt") ("content-type" "text/plain"))) (assert (or - (server-message-contains? "content of info.txt") - (server-message-contains? "Y29udGVudCBvZiBpbmZvLnR4dA==")) + (server-message-contains? "content of info file") + (server-message-contains? "Y29udGVudCBvZiBpbmZvIGZpbGU=")) (string-append "Expected server to receive message with text attachment, but received: " (server-message-data server))) (assert (server-message-contains-ci? "content-disposition: attachment") (string-append "Expected server to receive message with content-disposition: attachment, but received: " (server-message-data server))))) + (test "basic multiple text attachments" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("file-name" "info.txt") + ("content-type" "text/plain")) + '(("data" "info.txt") + ("file-name" "second-info.txt") + ("content-type" "text/plain"))) + (assert + (and (server-message-contains-ci? "info.txt") (server-message-contains-ci? "second-info.txt")) + (string-append "Expected server to receive message with two attachments, but received: " (server-message-data server))))) + (test "basic image attachment" (lambda () (sendmail-send-with-attachments '(("data" "test.png") @@ -388,18 +406,61 @@ (server-message-contains-ci? "content-disposition: attachment") (string-append "Expected server to receive message with content-disposition: attachment, but received: " (server-message-data server))))) - ; - Multiple attachments with the same name - ; - Attachment without a name - ; - inline vs attachment content-disposition - ; - inline attachment with ?cid? - ; - content/type multipart/mixed? + (test "multiple attachments with same name" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("file-name" "info.txt") + ("content-type" "text/plain")) + + '(("data" "info.txt") + ("file-name" "info.txt") + ("content-type" "text/plain"))) + (assert + (= 2 (length (string-contains-every? (server-message-data server) "info.txt"))) + (string-append "Expected server to receive message with two text attachments with the same name, but received: " (server-message-data server))))) + + ; Filename property is optional: https://datatracker.ietf.org/doc/html/rfc2183#section-1 + (test "attachment without a name" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("content-type" "text/plain"))) + (assert + (server-message-contains-ci? "content-disposition: attachment") + (string-append "Expected server to receive a message with an attachment, but received: " (server-message-data server))) + (assert + (not (server-message-contains-ci? "info.txt")) + (string-append "Expected server to receive a message with an attachment without a name, but received: " (server-message-data server))))) + + (test "image attachment with inline disposition" (lambda () + (sendmail-send-with-attachments + '(("data" "test.png") + ("file-name" "test.png") + ("content-type" "image/png") + ("content-disposition" "inline"))) + (assert + (server-message-contains-ci? "content-disposition: inline") + (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + + (test "text attachment with inline disposition" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("file-name" "info.txt") + ("content-type" "text/plain") + ("content-disposition" "inline"))) + (assert + (server-message-contains-ci? "content-disposition: inline") + (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + + ; - inline attachment with cid? https://www.rfc-editor.org/rfc/rfc2392 + ; => non-trivial as we either need to do it completely manually or use a template engine. A template engine has + ; custom syntax, so we can not create a general scenario for it (capability 'automatic-mime-detection (list - + )) (capability 'unicode-file-name (list - + )) )) From 0b38f13d87eaae306839094d5ee7de51d36b0940 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 12:35:16 +0200 Subject: [PATCH 08/11] [Py] Slight adaptations to new sendmail tests --- rosetta-test-py/sendmail-python-emails.py | 5 ++++- rosetta-test-py/sendmail-redmail.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rosetta-test-py/sendmail-python-emails.py b/rosetta-test-py/sendmail-python-emails.py index 6fbc671..7719178 100644 --- a/rosetta-test-py/sendmail-python-emails.py +++ b/rosetta-test-py/sendmail-python-emails.py @@ -132,7 +132,7 @@ def sendmail_send_message(env, sender:SMTPBackend, message, sender_address, reci headers=custom_headers,) for attachment in attachments: message_properties = { - "content_disposition": attachment.get("content-disposition", "attachment"), + "content_disposition": attachment["content-disposition"], } if "file-name" in attachment: @@ -171,6 +171,9 @@ def sendmail_error(env, result: SMTPResponse): # python-emails does mitigation for addresses but detection for other fields sendmail_suite.run( + exclude=( + # python-emails does not support attachments without a name + "test_attachment_without_a_name",), exclude_capabilities=( "root.connection.lazy-connection", # TODO: python-emails does not handle failed auth correctly "root.connection.eager-connection", diff --git a/rosetta-test-py/sendmail-redmail.py b/rosetta-test-py/sendmail-redmail.py index 71dad40..2aeb1a9 100644 --- a/rosetta-test-py/sendmail-redmail.py +++ b/rosetta-test-py/sendmail-redmail.py @@ -137,7 +137,7 @@ def sendmail_send_message(env, sender: EmailSender, message, sender_address, rec redmail_attachments = { attachment["file-name"]: pathlib.Path(rosetta.fixture_path('sendmail-fixtures/' + attachment["data"])) for attachment in attachments - if attachment.get("content-disposition", "attachment") == "attachment"} + if attachment["content-disposition"] == "attachment"} sender.send(sender=sender_address, receivers=recipient_addresses, cc=cc_addresses, @@ -172,6 +172,16 @@ def sendmail_error(env, result): sendmail_suite.run( exclude=( + # redmail does not support inline disposition + "test_text_attachment_with_inline_disposition", + "test_image_attachment_with_inline_disposition", + + # redmail does not support attachments without a filename + "test_attachment_without_a_name", + + # redmail does not support multiple attachments with the same name + "test_multiple_attachments_with_same_name", + "test_CRLF_detection_in_send-message_recipient", "test_CRLF_mitigation_in_send-message_sender", "test_Connect_with_invalid_credentials"), # TODO redmail leaks sockets when credentials are invalid From 2003bc79f22a56ed0bb4f97f20899a8dcc5f3b58 Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 15:57:05 +0200 Subject: [PATCH 09/11] Adds more attachment tests --- rosetta-test-suites/sendmail.ros | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index e55d1af..21ba76d 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -457,10 +457,23 @@ (capability 'automatic-mime-detection (list + ; This is difficult to test, as we would need to find the attachment MIME part and check + ; the MIME type of the attachment. + )) (capability 'unicode-file-name (list - + + (test "basic single text attachment with unicode file name" (lambda () + (sendmail-send-with-attachments + '(("data" "info.txt") + ("file-name" "info¡.txt") + ("content-type" "text/plain"))) + (assert + (server-message-contains-ci? "utf-8''info%C2%A1.txt") + (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + + )) )) From 3010ba810d96dde0dc465ffa016be109d01f77af Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 16:11:45 +0200 Subject: [PATCH 10/11] Shortened some tests by introducing a custom send procedure --- rosetta-test-suites/sendmail.ros | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index 21ba76d..4c00f45 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -33,8 +33,6 @@ (sendmail-send-message-full connection message-content from to-list '() '() (make-hash-table) (make-hash-table) message-options to-list-options)) (define (sendmail-send-message connection content from to-list) (sendmail-send-message-with-options connection content from to-list '() '())) - (define (sendmail-send-message-with-headers connection content from to-list headers-hash-map) - (sendmail-send-message-full connection content from to-list '() '() headers-hash-map (make-hash-table) '() '())) ; Response accessors (placeholder '(send-success? response) "Return whether the sending was successful.") @@ -275,12 +273,16 @@ (sendmail-disconnect smtp-connection) (set! smtp-connection '()))) + (define (sendmail-send-with-headers headers) + (sendmail-send-message-full + smtp-connection "message content" "sender@sender.to" '("user@recipient.to") '() '() (alist->hash-table headers) (make-hash-table) '() '())) + (test "set basic header" (lambda () - (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table '(("x-my-header" "some value")))) + (sendmail-send-with-headers '(("x-my-header" "some value"))) (assert (server-message-contains? "x-my-header: some value") (string-append "Expected x-my-header to be present, but instead got " (server-message-data server))))) (test "set header with unicode value" (lambda () - (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table '(("x-my-header" "¡some value")))) + (sendmail-send-with-headers '(("x-my-header" "¡some value"))) (assert (server-message-contains-ci? "x-my-header: =?utf-8?q?=C2=A1some?= value") (string-append "Expected x-my-header to contain encoded ¡, but instead got " (server-message-data server))) @@ -289,7 +291,7 @@ (string-append "Expected message content to be unaltered by unicode header value, but instead got" (server-message-data server))))) (test "set override standard header" (lambda () - (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table '(("To" "another-user@recipient.to")))) + (sendmail-send-with-headers '(("To" "another-user@recipient.to"))) (assert (server-message-contains? "To: another-user@recipient.to") (string-append "Expected To header field to include another-user..., but instead got " (server-message-data server))))) (capability 'crlf-injection (list @@ -300,14 +302,14 @@ '(("1some\rvalue") ("2some\nvalue") ("3some\r\nvalue")) (lambda (header-value) (let - ((responses (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table (list (list "x-my-header" header-value)))))) + ((responses (sendmail-send-with-headers (list (list "x-my-header" header-value))))) (assert (all? send-error? responses) (string-append "The header value: " header-value " should have resulted in error but did not:" responses))))) (data-test "CRLF detection in unicode header value" '(("1¡some\rvalue") ("2¡some\nvalue") ("3¡some\r\nvalue")) (lambda (header-value) (let - ((responses (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table (list (list "x-my-header" header-value)))))) + ((responses (sendmail-send-with-headers (list (list "x-my-header" header-value))))) (assert (all? send-error? responses) (string-append "The header value: " header-value " should have resulted in error but did not:" responses))))) )) @@ -318,7 +320,7 @@ '(("1some\rvalue") ("2some\nvalue") ("3some\r\nvalue")) (lambda (header-value) (let - ((responses (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table (list (list "x-my-header" header-value)))))) + ((responses (sendmail-send-with-headers (list (list "x-my-header" header-value))))) (assert (all? send-success? responses) (string-append "Message was not sent successfully, instead got: " responses)) (assert (not (server-message-contains? header-value)) (string-append "The header value: " header-value " should have been stripped of CRLF but did not:" (server-message-data server)))))) @@ -326,7 +328,7 @@ '(("1¡some \rvalue" "\rvalue") ("2¡some \nvalue" "\nvalue") ("3¡some \r\nvalue" "\r\nvalue")) (lambda (header-value fragment) (let - ((responses (sendmail-send-message-with-headers smtp-connection "message content" "sender@sender.to" '("user@recipient.to") (alist->hash-table (list (list "x-my-header" header-value)))))) + ((responses (sendmail-send-with-headers (list (list "x-my-header" header-value))))) (assert (all? send-success? responses) (string-append "Message was not sent successfully, instead got: " responses)) (assert (not (server-message-contains? fragment)) (string-append "The header value: " header-value " should have been stripped of CRLF but did not: " (server-message-data server)))))) From 12118151f735b7dffd24cc382768c478dbeb09da Mon Sep 17 00:00:00 2001 From: patrick Date: Wed, 16 Apr 2025 16:39:21 +0200 Subject: [PATCH 11/11] Refactored sendmail suite to use custom server message assertions --- rosetta-test-suites/sendmail.ros | 50 ++++++++++++++------------------ 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/rosetta-test-suites/sendmail.ros b/rosetta-test-suites/sendmail.ros index 4c00f45..bdda5e5 100644 --- a/rosetta-test-suites/sendmail.ros +++ b/rosetta-test-suites/sendmail.ros @@ -64,6 +64,16 @@ (define (server-message-contains? content) (string-contains? (server-message-data server) content)) + (define (assert-server-message-contains? content message-property-description) + (assert + (server-message-contains? content) + (string-append "Expected server to receive message with " message-property-description ", but received: " (server-message-data server)))) + + (define (assert-server-message-contains-ci? content message-property-description) + (assert + (server-message-contains-ci? content) + (string-append "Expected server to receive message with " message-property-description ", but received: " (server-message-data server)))) + (define (server-message-contains-ci? content) (string-contains-ci? (server-message-data server) content)) @@ -180,7 +190,7 @@ (let ((responses (sendmail-send-message-full smtp-connection "message content" "sender@sender.to" '() '("user1@recipient.to") '() (make-hash-table) (make-hash-table) '() '()))) (assert (all? send-success? responses)) - (assert (server-message-contains? "user1@recipient.to")) + (assert-server-message-contains? "user1@recipient.to" "cc recipient") (assert (any? (lambda (request) @@ -279,20 +289,16 @@ (test "set basic header" (lambda () (sendmail-send-with-headers '(("x-my-header" "some value"))) - (assert (server-message-contains? "x-my-header: some value") (string-append "Expected x-my-header to be present, but instead got " (server-message-data server))))) + (assert-server-message-contains? "x-my-header" "x-my-header present"))) (test "set header with unicode value" (lambda () (sendmail-send-with-headers '(("x-my-header" "¡some value"))) - (assert - (server-message-contains-ci? "x-my-header: =?utf-8?q?=C2=A1some?= value") - (string-append "Expected x-my-header to contain encoded ¡, but instead got " (server-message-data server))) - (assert - (server-message-contains? "message content") - (string-append "Expected message content to be unaltered by unicode header value, but instead got" (server-message-data server))))) + (assert-server-message-contains? "x-my-header: =?utf-8?q?=C2=A1some?= value" "x-my-header containing encoded ¡") + (assert-server-message-contains? "message content" "unaltered message content despite unicode header value"))) (test "set override standard header" (lambda () (sendmail-send-with-headers '(("To" "another-user@recipient.to"))) - (assert (server-message-contains? "To: another-user@recipient.to") (string-append "Expected To header field to include another-user..., but instead got " (server-message-data server))))) + (assert-server-message-contains? "To: another-user@recipient.to" "header field that includes another-user@recipient.to"))) (capability 'crlf-injection (list @@ -426,9 +432,7 @@ (sendmail-send-with-attachments '(("data" "info.txt") ("content-type" "text/plain"))) - (assert - (server-message-contains-ci? "content-disposition: attachment") - (string-append "Expected server to receive a message with an attachment, but received: " (server-message-data server))) + (assert-server-message-contains-ci? "content-disposition: attachment" "an attachment") (assert (not (server-message-contains-ci? "info.txt")) (string-append "Expected server to receive a message with an attachment without a name, but received: " (server-message-data server))))) @@ -439,9 +443,7 @@ ("file-name" "test.png") ("content-type" "image/png") ("content-disposition" "inline"))) - (assert - (server-message-contains-ci? "content-disposition: inline") - (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + (assert-server-message-contains-ci? "content-disposition: inline" "inline attachment"))) (test "text attachment with inline disposition" (lambda () (sendmail-send-with-attachments @@ -449,9 +451,7 @@ ("file-name" "info.txt") ("content-type" "text/plain") ("content-disposition" "inline"))) - (assert - (server-message-contains-ci? "content-disposition: inline") - (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + (assert-server-message-contains-ci? "content-disposition: inline" "inline attachment"))) ; - inline attachment with cid? https://www.rfc-editor.org/rfc/rfc2392 ; => non-trivial as we either need to do it completely manually or use a template engine. A template engine has @@ -471,9 +471,7 @@ '(("data" "info.txt") ("file-name" "info¡.txt") ("content-type" "text/plain"))) - (assert - (server-message-contains-ci? "utf-8''info%C2%A1.txt") - (string-append "Expected server to receive a message with an inline attachment, but received: " (server-message-data server))))) + (assert-server-message-contains-ci? "utf-8''info%C2%A1.txt" "unicode file name"))) )) @@ -605,10 +603,7 @@ smtp-connection "¡a test message containing unicode!" "sender@sender.com" '("user@recipient.com") '("BODY=8BITMIME") '()))) (assert (all? send-success? send-message-responses)) (assert-any-request server "MAIL FROM: BODY=8BITMIME") - (assert - (server-message-contains? "¡a test message containing unicode!") - (string-append - "Expected server to receive message with unicode content directly, but received: " (server-message-data server)))))) + (assert-server-message-contains? "¡a test message containing unicode!" "unicode content")))) )) @@ -632,10 +627,7 @@ smtp-connection "¡a test message containing unicode!" "sender@sender.com" '("user@recipient.com") '() '()))) (assert (all? send-success? send-message-responses)) (assert-any-request server "MAIL FROM: BODY=8BITMIME") - (assert - (server-message-contains? "¡a test message containing unicode!") - (string-append - "Expected server to receive message with unicode content directly, but received: " (server-message-data server)))))) + (assert-server-message-contains? "¡a test message containing unicode!" "unicode content directly")))) )) ))