mesh/merge-gate out of date: judged against trunk e22326240833, the trunk is now 482232e8c51c; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk e22326240833, the trunk is now 482232e8c51c; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
ADR 0359 decided one module holds the operator's mail accounts. This is its first stage: an imap adapter that keeps IDLE on the inbox and polls the other folders read-only, indexes each message in its own database, keeps each attachment once by its SHA-256 in its bucket, and says every added, changed or removed message as a metadata-only event. Bodies and attachments are served only by its tools, to callers the trusted setting grants names. Its health waits for the account's own secret until it is given at the desk (issue 710).
105 lines
3.5 KiB
Go
105 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The signature is AWS Signature Version 4 as S3 documents it: its own worked example, "GET Object", signs to the
|
|
// signature AWS publishes.
|
|
func TestTheSignatureIsTheOneS3Documents(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, "https://examplebucket.s3.amazonaws.com/test.txt", nil)
|
|
req.Header.Set("Range", "bytes=0-9")
|
|
Sign(req, nil, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1", time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC))
|
|
want := "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, " +
|
|
"SignedHeaders=host;range;x-amz-content-sha256;x-amz-date, Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
|
|
if got := req.Header.Get("Authorization"); got != want {
|
|
t.Fatalf("signed\n%s\nnot\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
// An attachment is kept once by its SHA-256: the same file twice is one PUT, and it reads back.
|
|
func TestAnAttachmentIsKeptOnceByItsHash(t *testing.T) {
|
|
var mu sync.Mutex
|
|
kept := map[string][]byte{}
|
|
puts := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256 Credential=mesh_novox_mail/") {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
switch r.Method {
|
|
case http.MethodHead:
|
|
if _, ok := kept[r.URL.Path]; !ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
case http.MethodPut:
|
|
body, _ := io.ReadAll(r.Body)
|
|
kept[r.URL.Path] = body
|
|
puts++
|
|
case http.MethodGet:
|
|
b, ok := kept[r.URL.Path]
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
_, _ = w.Write(b)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "store.json")
|
|
host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
|
|
writeFile(t, path, `{"scheme": "http", "at": "`+host+`", "port": "`+port+`", "bucket": "mesh-novox-mail", "region": "eu-west",
|
|
"key": "mesh_novox_mail", "secret": "s3cr3t"}`)
|
|
facts, err := ReadStoreFacts(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3 := NewS3(facts)
|
|
ctx := context.Background()
|
|
a, err := Keep(ctx, s3, []byte("same file"), "text/plain", "a.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := Keep(ctx, s3, []byte("same file"), "text/plain", "b.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Key != b.Key || a.Bucket != "mesh-novox-mail" || puts != 1 || len(a.Key) != 64 {
|
|
t.Fatalf("%+v %+v, %d put(s)", a, b, puts)
|
|
}
|
|
if _, ok := kept["/mesh-novox-mail/"+a.Key]; !ok {
|
|
t.Fatalf("kept %v", kept)
|
|
}
|
|
got, err := s3.Get(ctx, a.Key)
|
|
if err != nil || string(got) != "same file" {
|
|
t.Fatalf("%q %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAMessagesTextAndAttachmentsAreRead(t *testing.T) {
|
|
p, err := ParseMessage([]byte(strings.ReplaceAll(attachedMail("x"), "\n", "\r\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.TrimSpace(p.Text) != "Please find the invoice attached." || len(p.Attachments) != 1 ||
|
|
p.Attachments[0].Name != "invoice.pdf" || p.Attachments[0].Type != "application/pdf" || string(p.Attachments[0].Data) != attachmentData {
|
|
t.Fatalf("%+v", p)
|
|
}
|
|
html := "From: a@b.c\r\nSubject: s\r\nContent-Type: text/html; charset=utf-8\r\n\r\n<html><head><style>p{}</style></head><body><p>Hello & welcome</p><script>alert(1)</script><p>Bye</p></body></html>"
|
|
p, err = ParseMessage([]byte(html))
|
|
if err != nil || p.Text != "Hello & welcome\nBye" {
|
|
t.Fatalf("%q %v", p.Text, err)
|
|
}
|
|
}
|