1package service23import (4 "bytes"5 "context"6 "fmt"7 "log/slog"8 "os"9 "slices"10 "strings"11 "testing"12 "time"1314 "git.lin.moe/go/mlisting/storage"15 "git.lin.moe/go/mlisting/storage/sqlite"16 "git.lin.moe/go/mlisting/tools/testdata"17 "github.com/emersion/go-message/mail"18 "github.com/emersion/go-message/textproto"19 "github.com/emersion/go-smtp"20)2122var (23 st *sqlite.Storage24 private_list, strict_list, public_list storage.List25 memberPerms = map[string]uint8{26 "admin@base.lan": storage.PERM_BROWSE | storage.PERM_REPLY | storage.PERM_POST,27 "member@base.lan": storage.PERM_BROWSE | storage.PERM_REPLY,28 "reader@base.lan": storage.PERM_BROWSE,29 }30 members = []string{"admin@base.lan", "member@base.lan", "reader@base.lan"}31)3233const (34 PRIVATE_LIST = "private@base.lan" // default: storage.PERM_BROWSE35 STRICT_LIST = "strict@base.lan" // default: storage.PERM_BROWSE | storage.PERM_REPLY36 PUBLIC_LIST = "public@base.lan" // default: storage.PERM_BROWSE | storage.PERM_REPLY | storage.PERM_POST,37)3839func TestMain(m *testing.M) {40 var err error41 st, err = sqlite.NewStorage(":memory:")42 if err != nil {43 panic(err)44 }45 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*30)46 defer cancel()47 defer st.Shutdown(ctx)4849 if private_list, err = st.NewList(ctx,50 "private", PRIVATE_LIST, "",51 storage.PERM_BROWSE); err != nil {52 panic(err)53 }54 if strict_list, err = st.NewList(ctx,55 "strict", STRICT_LIST, "",56 storage.PERM_BROWSE|storage.PERM_REPLY); err != nil {57 panic(err)58 }59 if public_list, err = st.NewList(ctx,60 "public", PUBLIC_LIST, "",61 storage.PERM_BROWSE|storage.PERM_REPLY|storage.PERM_POST); err != nil {62 panic(err)63 }6465 for mem := range memberPerms {66 private_list.NewMember(ctx, mem)67 private_list.UpdateMember(ctx, mem, memberPerms[mem])6869 strict_list.NewMember(ctx, mem)70 strict_list.UpdateMember(ctx, mem, memberPerms[mem])7172 public_list.NewMember(ctx, mem)73 public_list.UpdateMember(ctx, mem, memberPerms[mem])74 }7576 m.Run()77}7879func testPostMessage(ctx context.Context, mta MTA, from string, rcpt storage.List) error {80 ctx, cancel := context.WithTimeout(ctx, time.Second*10)81 defer cancel()8283 var msg = testdata.TestMessages[0]84 var buf = bytes.NewBuffer(nil)85 header := mail.HeaderFromMap(msg.Header)86 header.Set("From", from)87 header.Set("To", rcpt.Address())88 if err := header.GenerateMessageID(); err != nil {89 panic(err)90 }9192 textproto.WriteHeader(buf, header.Header.Header)93 buf.Write(msg.Body)9495 sess := lmtpSession{96 ctx: ctx,97 cancel: cancel,98 storage: st,99 logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{100 Level: slog.LevelDebug,101 })),102 mta: mta,103 maxSize: 0,104 List: nil,105 Cmd: "",106 }107 if err := sess.Mail(from, &smtp.MailOptions{}); err != nil {108 return err109 }110 if err := sess.Rcpt(rcpt.Address(), &smtp.RcptOptions{}); err != nil {111 return err112 }113 if err := sess.LMTPData(buf, nil); err != nil {114 return err115 }116117 return nil118}119120func testReplyMessage(ctx context.Context, mta MTA, from string, rcpt storage.List) error {121 ctx, cancel := context.WithTimeout(ctx, time.Second*10)122 defer cancel()123124 // message replied to125 var msg = testdata.TestMessages[0]126 header := mail.HeaderFromMap(msg.Header)127 if err := header.GenerateMessageID(); err != nil {128 panic(err)129 }130 msgId, _ := header.MessageID()131132 if _, err := rcpt.AddMessage(ctx, header.Map(), msg.Body); err != nil {133 panic(err)134 }135136 msg = testdata.TestMessages[1]137 var buf = bytes.NewBuffer(nil)138 header = mail.HeaderFromMap(msg.Header)139 header.Set("From", from)140 header.Set("To", rcpt.Address())141 if err := header.GenerateMessageID(); err != nil {142 panic(err)143 }144 header.SetMsgIDList("In-Reply-To", []string{msgId})145 textproto.WriteHeader(buf, header.Header.Header)146 buf.Write(msg.Body)147 sess := lmtpSession{148 ctx: ctx,149 cancel: cancel,150 storage: st,151 logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{152 Level: slog.LevelDebug,153 })),154 mta: mta,155 maxSize: 0,156 List: nil,157 Cmd: "",158 }159 if err := sess.Mail(from, &smtp.MailOptions{}); err != nil {160 return err161 }162 if err := sess.Rcpt(rcpt.Address(), &smtp.RcptOptions{}); err != nil {163 return err164 }165 if err := sess.LMTPData(buf, nil); err != nil {166 return err167 }168169 return nil170}171172func TestForwardMessage(t *testing.T) {173 ctx := context.TODO()174175 for _, list := range []storage.List{private_list, strict_list, public_list} {176 mta := MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {177 if list.DefaultPerm()&storage.PERM_POST != 0 {178 exp_rcpts := make([]string, len(members))179 copy(exp_rcpts, members)180 slices.Sort(exp_rcpts)181 slices.Sort(rcpts)182183 if !slices.Equal(exp_rcpts, rcpts) {184 t.Fatalf("post a new thread failed: rcpts: %v, expect_rcpts: %v",185 rcpts, exp_rcpts)186 }187 } else {188 // message of dening post189 if !slices.Equal([]string{"any@base.lan"}, rcpts) {190 t.Fatalf("expect a reject message, rcpts:%v, expect_rcpts: %v",191 rcpts, []string{"any@base.lan"})192 }193 }194 return nil195 })196 if err := testPostMessage(ctx, &mta, "any@base.lan", list); err != nil {197 t.Fatal(err)198 }199200 mta = MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {201 if list.DefaultPerm()&storage.PERM_REPLY != 0 {202 exp_rcpts := make([]string, len(members))203 copy(exp_rcpts, members)204 slices.Sort(exp_rcpts)205 slices.Sort(rcpts)206207 if !slices.Equal(exp_rcpts, rcpts) {208 t.Fatalf("reply a thread failed: rcpts: %v, expect_rcpts: %v",209 rcpts, exp_rcpts)210 }211 } else {212 if !slices.Equal([]string{"any@base.lan"}, rcpts) {213 t.Fatalf("expect a reject message, rcpts:%v, expect_rcpts: %v",214 rcpts, []string{"any@base.lan"})215 }216 }217 return nil218 })219 if err := testReplyMessage(ctx, &mta, "any@base.lan", list); err != nil {220 t.Fatal(err)221 }222223 for mem := range memberPerms {224 mta := MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {225 if memberPerms[mem]&storage.PERM_POST != 0 {226 exp_rcpts := make([]string, len(members))227 copy(exp_rcpts, members)228 exp_rcpts = slices.DeleteFunc(exp_rcpts, func(i string) bool { return i == mem })229 slices.Sort(exp_rcpts)230 slices.Sort(rcpts)231232 if !slices.Equal(exp_rcpts, rcpts) {233 t.Fatalf("post a new thread failed: rcpts: %v, expect_rcpts: %v",234 rcpts, exp_rcpts)235 }236 } else {237 if !slices.Equal([]string{mem}, rcpts) {238 t.Fatalf("expect a reject message, rcpts:%v, expect_rcpts: %v",239 rcpts, []string{mem})240 }241 }242 return nil243 })244 if err := testPostMessage(ctx, &mta, mem, list); err != nil {245 t.Fatal(err)246 }247248 mta = MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {249 if memberPerms[mem]&storage.PERM_REPLY != 0 {250 exp_rcpts := make([]string, len(members))251 copy(exp_rcpts, members)252 exp_rcpts = slices.DeleteFunc(exp_rcpts, func(i string) bool { return i == mem })253 slices.Sort(exp_rcpts)254 slices.Sort(rcpts)255256 if !slices.Equal(exp_rcpts, rcpts) {257 t.Fatalf("reply a thread failed: rcpts: %v, expect_rcpts: %v",258 rcpts, exp_rcpts)259 }260 } else {261 if !slices.Equal([]string{mem}, rcpts) {262 t.Fatalf("expect a reject message, rcpts:%v, expect_rcpts: %v",263 rcpts, []string{"any@base.lan"})264 }265 }266 return nil267 })268 if err := testReplyMessage(ctx, &mta, mem, list); err != nil {269 t.Fatal(err)270 }271 }272 }273}274275func TestSubscribe(t *testing.T) {276 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)277 defer cancel()278279 const MEM = "test-sub@base.lan"280 logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{281 Level: slog.LevelDebug,282 }))283284 var msg = testdata.TestMessages[0]285 var buf = bytes.NewBuffer(nil)286 header := mail.HeaderFromMap(msg.Header)287 header.Set("From", MEM)288 rcpt := (&Address{Address: public_list.Address()}).WithAction(CMD_SUBSCRIBE).String()289 header.Set("To", rcpt)290 if err := header.GenerateMessageID(); err != nil {291 panic(err)292 }293294 textproto.WriteHeader(buf, header.Header.Header)295 buf.Write(msg.Body)296297 var token string298 mta := MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {299 logger.Debug("send out message", "from", header.Get("From"), "rcpts", rcpts)300 token = strings.TrimPrefix(header.Get("Subject"), "Confirm ")301 return nil302 })303 sess := lmtpSession{304 ctx: ctx,305 cancel: cancel,306 storage: st,307 logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{308 Level: slog.LevelDebug,309 })),310 mta: &mta,311 maxSize: 0,312 List: nil,313 Cmd: "",314 }315 if err := sess.Mail(MEM, &smtp.MailOptions{}); err != nil {316 t.Fatal(err)317 }318 if err := sess.Rcpt(rcpt, &smtp.RcptOptions{}); err != nil {319 t.Fatal(err)320 }321 if err := sess.LMTPData(buf, nil); err != nil {322 t.Fatal(err)323 }324325 msg = testdata.TestMessages[1]326 buf = bytes.NewBuffer(nil)327 header = mail.HeaderFromMap(msg.Header)328 header.Set("From", MEM)329 rcpt = (&Address{Address: public_list.Address()}).WithAction(CMD_CONFIRM).String()330 header.Set("To", rcpt)331 header.Set("Subject", fmt.Sprintf("Re: Confirm %s", token))332 if err := header.GenerateMessageID(); err != nil {333 panic(err)334 }335336 textproto.WriteHeader(buf, header.Header.Header)337 buf.Write(msg.Body)338 mta = MockMTA(func(header textproto.Header, body []byte, sender string, rcpts []string) error {339 logger.Debug("send out message", "from", header.Get("From"), "rcpts", rcpts)340 return nil341 })342 sess = lmtpSession{343 ctx: ctx,344 cancel: cancel,345 storage: st,346 logger: logger,347 mta: &mta,348 maxSize: 0,349 List: nil,350 Cmd: "",351 }352 if err := sess.Mail(MEM, &smtp.MailOptions{}); err != nil {353 t.Fatal(err)354 }355 if err := sess.Rcpt(rcpt, &smtp.RcptOptions{}); err != nil {356 t.Fatal(err)357 }358 if err := sess.LMTPData(buf, nil); err != nil {359 t.Fatal(err)360 }361362 _, err := public_list.GetMember(ctx, MEM)363 if err != nil {364 t.Fatal(err)365 }366}