Start coding the mailbox api

This commit is contained in:
Melon 2023-08-13 03:04:16 +01:00
parent 5f0cd27a29
commit e84e0e3d4b
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 349 additions and 0 deletions

15
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,15 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.21.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
- run: go build ./cmd/primrose/
- run: go test ./...

58
api/api.go Normal file
View File

@ -0,0 +1,58 @@
package api
import (
"encoding/json"
"github.com/1f349/primrose/imap"
"github.com/1f349/primrose/smtp"
"github.com/julienschmidt/httprouter"
"net/http"
"time"
)
type Conf struct {
Listen string `yaml:"listen"`
}
func SetupApiServer(conf Conf, send *smtp.Smtp, recv *imap.Imap) *http.Server {
r := httprouter.New()
// smtp
r.POST("/message", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
// check body exists
if req.Body == nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
// parse json body
var j smtp.Json
err := json.NewDecoder(req.Body).Decode(&j)
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
mail, err := j.PrepareMail()
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
if send.Send(mail) != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(http.StatusAccepted)
})
return &http.Server{
Addr: conf.Listen,
Handler: r,
ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Minute,
MaxHeaderBytes: 2500,
}
}

13
cmd/primrose/conf.go Normal file
View File

@ -0,0 +1,13 @@
package main
import (
"github.com/1f349/primrose/api"
"github.com/1f349/primrose/imap"
"github.com/1f349/primrose/smtp"
)
type Conf struct {
Smtp smtp.Smtp `yaml:"smtp"`
Imap imap.Imap `yaml:"imap"`
Api api.Conf `yaml:"api"`
}

5
cmd/primrose/main.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
// TODO(Melon): write start up code
}

56
imap/client.go Normal file
View File

@ -0,0 +1,56 @@
package imap
import (
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
var imapStatusFlags = []imap.StatusItem{
imap.StatusMessages,
imap.StatusRecent,
imap.StatusUidNext,
imap.StatusUidValidity,
imap.StatusUnseen,
}
type Client struct {
ic *client.Client
}
func (c *Client) Status(folder string) (*imap.MailboxStatus, error) {
mbox, err := c.ic.Status(folder, imapStatusFlags)
return mbox, err
}
func (c *Client) Fetch(folder string, start, end, limit uint32) ([]*imap.Message, error) {
// select the mailbox
mbox, err := c.ic.Select(folder, false)
if err != nil {
return nil, err
}
// setup fetch range
if end > mbox.Messages {
end = mbox.Messages
}
if end-start > limit {
start = end - (limit - 1)
}
seqSet := new(imap.SeqSet)
seqSet.AddRange(start, end)
messages := make(chan *imap.Message, limit)
done := make(chan error, 1)
go func() {
done <- c.ic.Fetch(seqSet, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
outMsg := make([]*imap.Message, 0, limit)
for msg := range messages {
outMsg = append(outMsg, msg)
}
if err := <-done; err != nil {
return nil, err
}
return outMsg, nil
}

35
imap/imap.go Normal file
View File

@ -0,0 +1,35 @@
package imap
import (
"fmt"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
)
type Imap struct {
Server string `yaml:"server"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Separator string `yaml:"separator"`
}
func (i *Imap) MakeClient(user string) (*Client, error) {
// dial imap server
imapClient, err := client.Dial(i.Server)
if err != nil {
return nil, err
}
// prepare login details
un := fmt.Sprintf("%s%s%s", user, i.Separator, i.Username)
saslLogin := sasl.NewPlainClient("", un, i.Password)
// authenticate
err = imapClient.Authenticate(saslLogin)
if err != nil {
return nil, err
}
// new client
return &Client{ic: imapClient}, nil
}

101
smtp/json.go Normal file
View File

@ -0,0 +1,101 @@
package smtp
import (
"bytes"
"errors"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"strings"
"time"
)
var (
ErrInvalidBodyType = errors.New("invalid body type")
ErrMultipleFromAddresses = errors.New("multiple from addresses")
)
type Json struct {
From string `json:"from"`
ReplyTo string `json:"reply_to"`
To string `json:"to"`
Cc string `json:"cc"`
Bcc string `json:"bcc"`
Subject string `json:"subject"`
BodyType string `json:"body_type"`
Body string `json:"body"`
}
func (s Json) parseAddresses() (addrFrom, addrReplyTo, addrTo, addrCc, addrBcc []*mail.Address, err error) {
// parse addresses
addrFrom, err = mail.ParseAddressList(s.From)
if err != nil {
return
}
addrReplyTo, err = mail.ParseAddressList(s.ReplyTo)
if err != nil {
return
}
addrTo, err = mail.ParseAddressList(s.To)
if err != nil {
return
}
addrCc, err = mail.ParseAddressList(s.Cc)
if err != nil {
return
}
addrBcc, err = mail.ParseAddressList(s.Bcc)
return
}
func (s Json) PrepareMail() (*Mail, error) {
// parse addresses from json data
addrFrom, addrReplyTo, addrTo, addrCc, addrBcc, err := s.parseAddresses()
if err != nil {
return nil, err
}
// only one from address allowed here
if len(addrFrom) != 1 {
return nil, ErrMultipleFromAddresses
}
// save for use in the caller
from := addrFrom[0].Address
// set base headers
var h mail.Header
h.SetDate(time.Now())
h.SetSubject(s.Subject)
h.SetAddressList("From", addrFrom)
h.SetAddressList("Reply-To", addrReplyTo)
h.SetAddressList("To", addrTo)
h.SetAddressList("Cc", addrCc)
// set content type header
switch s.BodyType {
case "plain":
h.Set("Content-Type", "text/plain; charset=utf-8")
case "html":
h.Set("Content-Type", "text/html; charset=utf-8")
default:
return nil, ErrInvalidBodyType
}
entity, err := message.New(h.Header, strings.NewReader(s.Body))
if err != nil {
return nil, err
}
m := &Mail{
from: from,
deliver: CreateSenderSlice(addrTo, addrCc, addrBcc),
}
out := new(bytes.Buffer)
if err := entity.WriteTo(out); err != nil {
return nil, err
}
m.body = out.Bytes()
return m, nil
}

45
smtp/smtp.go Normal file
View File

@ -0,0 +1,45 @@
package smtp
import (
"bytes"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp"
)
type Smtp struct {
Server string `yaml:"server"`
}
type Mail struct {
from string
deliver []string
body []byte
}
func (s *Smtp) Send(mail *Mail) error {
// dial smtp server
smtpClient, err := smtp.Dial(s.Server)
if err != nil {
return err
}
// use a reader to send bytes
r := bytes.NewReader(mail.body)
// send mail
return smtpClient.SendMail(mail.from, mail.deliver, r)
}
func CreateSenderSlice(to, cc, bcc []*mail.Address) []string {
a := make([]string, 0, len(to)+len(cc)+len(bcc))
for _, i := range to {
a = append(a, i.Address)
}
for _, i := range cc {
a = append(a, i.Address)
}
for _, i := range bcc {
a = append(a, i.Address)
}
return a
}

21
smtp/smtp_test.go Normal file
View File

@ -0,0 +1,21 @@
package smtp
import (
"github.com/emersion/go-message/mail"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateSenderSlice(t *testing.T) {
a := []*mail.Address{{Address: "a@example.com"}, {Address: "b@example.com"}}
b := []*mail.Address{{Address: "a@example.com"}, {Address: "c@example.com"}}
c := []*mail.Address{{Address: "a@example.com"}, {Address: "d@example.com"}}
assert.Equal(t, []string{
"a@example.com",
"b@example.com",
"a@example.com",
"c@example.com",
"a@example.com",
"d@example.com",
}, CreateSenderSlice(a, b, c))
}