mirror of
https://github.com/1f349/lotus.git
synced 2024-11-09 14:42:56 +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