mirror of
https://github.com/1f349/lotus.git
synced 2024-12-22 08:04:06 +00:00
Start coding the mailbox api
This commit is contained in:
parent
5f0cd27a29
commit
e84e0e3d4b
15
.github/workflows/test.yml
vendored
Normal file
15
.github/workflows/test.yml
vendored
Normal 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
58
api/api.go
Normal 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
13
cmd/primrose/conf.go
Normal 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
5
cmd/primrose/main.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// TODO(Melon): write start up code
|
||||||
|
}
|
56
imap/client.go
Normal file
56
imap/client.go
Normal 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
35
imap/imap.go
Normal 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
101
smtp/json.go
Normal 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
45
smtp/smtp.go
Normal 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
21
smtp/smtp_test.go
Normal 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))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user