github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/mails/mail_test.go (about) 1 package mails 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "net" 8 "net/textproto" 9 "strconv" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/cozy/cozy-stack/model/instance/lifecycle" 15 "github.com/cozy/cozy-stack/model/job" 16 "github.com/cozy/cozy-stack/pkg/config/config" 17 "github.com/cozy/cozy-stack/pkg/mail" 18 "github.com/cozy/cozy-stack/tests/testutils" 19 "github.com/cozy/gomail" 20 "github.com/stretchr/testify/assert" 21 ) 22 23 const serverString = `220 hello world 24 502 EH? 25 250 smtp.me at your service 26 250 Sender ok 27 250 Receiver ok 28 354 Go ahead 29 250 Data ok 30 221 Goodbye 31 ` 32 33 func TestMails(t *testing.T) { 34 if testing.Short() { 35 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 36 } 37 38 config.UseTestFile(t) 39 40 setup := testutils.NewSetup(t, t.Name()) 41 42 inst := setup.GetTestInstance(&lifecycle.Options{Email: "me@me"}) 43 44 t.Run("mail send server", func(t *testing.T) { 45 clientStrings := []string{`EHLO localhost 46 HELO localhost 47 MAIL FROM:<me@me> 48 RCPT TO:<you1@you> 49 DATA 50 Hey !!! 51 . 52 QUIT 53 `} 54 55 expectedHeaders := map[string]string{ 56 "From": "me@me", 57 "To": "you1@you", 58 "Subject": "Up?", 59 "Date": "Mon, 01 Jan 0001 00:00:00 +0000", 60 "Content-Transfer-Encoding": "quoted-printable", 61 "Content-Type": "text/plain; charset=UTF-8", 62 "Mime-Version": "1.0", 63 "X-Cozy": "cozy.example.com", 64 } 65 66 mailServer(t, serverString, clientStrings, expectedHeaders, func(host string, port int) error { 67 msg := &mail.Options{ 68 From: &mail.Address{Email: "me@me"}, 69 To: []*mail.Address{ 70 {Email: "you1@you"}, 71 }, 72 Date: &time.Time{}, 73 Subject: "Up?", 74 Dialer: &gomail.DialerOptions{ 75 Host: host, 76 Port: port, 77 DisableTLS: true, 78 }, 79 Parts: []*mail.Part{ 80 { 81 Body: "Hey !!!", 82 Type: "text/plain", 83 }, 84 }, 85 Locale: "en", 86 } 87 j := &job.Job{JobID: "1", Domain: "cozy.example.com"} 88 ctx, cancel := job.NewTaskContext("0", j, nil) 89 defer cancel() 90 return sendMail(ctx, msg, "cozy.example.com") 91 }) 92 }) 93 94 t.Run("send template mail", func(t *testing.T) { 95 clientStrings := []string{`EHLO localhost 96 HELO localhost 97 MAIL FROM:<me@me> 98 RCPT TO:<you1@you> 99 DATA 100 <!DOCTYPE html> 101 <html> 102 <head> 103 <meta charset=3D"UTF-8"> 104 <title>My page</title> 105 </head> 106 <body> 107 <div>My photos</div><div>My blog</div> 108 </body> 109 </html> 110 . 111 QUIT 112 `} 113 114 expectedHeaders := map[string]string{ 115 "From": "me@me", 116 "To": "you1@you", 117 "Subject": "Up?", 118 "Date": "Mon, 01 Jan 0001 00:00:00 +0000", 119 "Content-Transfer-Encoding": "quoted-printable", 120 "Content-Type": "text/html; charset=UTF-8", 121 "Mime-Version": "1.0", 122 "X-Cozy": "cozy.example.com", 123 } 124 125 mailBody := `<!DOCTYPE html> 126 <html> 127 <head> 128 <meta charset="UTF-8"> 129 <title>My page</title> 130 </head> 131 <body> 132 <div>My photos</div><div>My blog</div> 133 </body> 134 </html> 135 ` 136 137 mailServer(t, serverString, clientStrings, expectedHeaders, func(host string, port int) error { 138 msg := &mail.Options{ 139 From: &mail.Address{Email: "me@me"}, 140 To: []*mail.Address{ 141 {Email: "you1@you"}, 142 }, 143 Date: &time.Time{}, 144 Subject: "Up?", 145 Dialer: &gomail.DialerOptions{ 146 Host: host, 147 Port: port, 148 DisableTLS: true, 149 }, 150 Parts: []*mail.Part{ 151 {Body: mailBody, Type: "text/html"}, 152 }, 153 Locale: "en", 154 } 155 j := &job.Job{JobID: "1", Domain: "cozy.example.com"} 156 ctx, cancel := job.NewTaskContext("0", j, nil) 157 defer cancel() 158 return sendMail(ctx, msg, "cozy.example.com") 159 }) 160 }) 161 162 t.Run("with missing subject", func(t *testing.T) { 163 msg := &mail.Options{ 164 From: &mail.Address{Email: "me@me"}, 165 To: []*mail.Address{{Email: "you@you"}}, 166 Locale: "en", 167 } 168 j := &job.Job{JobID: "1", Domain: "cozy.example.com"} 169 ctx, cancel := job.NewTaskContext("0", j, nil) 170 defer cancel() 171 err := sendMail(ctx, msg, "cozy.example.com") 172 if assert.Error(t, err) { 173 assert.Equal(t, "Missing mail subject", err.Error()) 174 } 175 }) 176 177 t.Run("with bad body type", func(t *testing.T) { 178 msg := &mail.Options{ 179 From: &mail.Address{Email: "me@me"}, 180 To: []*mail.Address{{Email: "you@you"}}, 181 Subject: "Up?", 182 Parts: []*mail.Part{ 183 { 184 Type: "text/qsdqsd", 185 Body: "foo", 186 }, 187 }, 188 Locale: "en", 189 } 190 j := &job.Job{JobID: "1", Domain: "cozy.example.com"} 191 ctx, cancel := job.NewTaskContext("0", j, nil) 192 defer cancel() 193 err := sendMail(ctx, msg, "cozy.example.com") 194 if assert.Error(t, err) { 195 assert.Equal(t, "Unknown body content-type text/qsdqsd", err.Error()) 196 } 197 }) 198 199 t.Run("send with NoReply", func(t *testing.T) { 200 sendMail = func(_ *job.TaskContext, opts *mail.Options, domain string) error { 201 assert.NotNil(t, opts.From) 202 assert.NotNil(t, opts.To) 203 assert.Len(t, opts.To, 1) 204 assert.Equal(t, "me@me", opts.To[0].Email) 205 assert.Equal(t, "noreply@"+inst.Domain, opts.From.Email) 206 assert.Equal(t, inst.Domain, domain) 207 return errors.New("yes") 208 } 209 defer func() { 210 sendMail = doSendMail 211 }() 212 msg, _ := job.NewMessage(mail.Options{ 213 Mode: "noreply", 214 Subject: "Up?", 215 Parts: []*mail.Part{ 216 { 217 Type: "text/plain", 218 Body: "foo", 219 }, 220 }, 221 Locale: "en", 222 }) 223 j := job.NewJob(inst, &job.JobRequest{ 224 Message: msg, 225 WorkerType: "sendmail", 226 }) 227 ctx, cancel := job.NewTaskContext("123", j, inst) 228 defer cancel() 229 err := SendMail(ctx) 230 if assert.Error(t, err) { 231 assert.Equal(t, "yes", err.Error()) 232 } 233 }) 234 235 t.Run("send with From", func(t *testing.T) { 236 sendMail = func(_ *job.TaskContext, opts *mail.Options, domain string) error { 237 assert.NotNil(t, opts.From) 238 assert.NotNil(t, opts.To) 239 assert.Len(t, opts.To, 1) 240 assert.Equal(t, "you@you", opts.To[0].Email) 241 assert.Equal(t, "noreply@"+inst.Domain, opts.From.Email) 242 assert.Equal(t, "me@me", opts.ReplyTo.Email) 243 assert.Equal(t, inst.Domain, domain) 244 return errors.New("yes") 245 } 246 defer func() { 247 sendMail = doSendMail 248 }() 249 msg, _ := job.NewMessage(mail.Options{ 250 Mode: "from", 251 Subject: "Up?", 252 To: []*mail.Address{{Email: "you@you"}}, 253 Parts: []*mail.Part{ 254 { 255 Type: "text/plain", 256 Body: "foo", 257 }, 258 }, 259 Locale: "en", 260 }) 261 j := job.NewJob(inst, &job.JobRequest{ 262 Message: msg, 263 WorkerType: "sendmail", 264 }) 265 ctx, cancel := job.NewTaskContext("123", j, inst) 266 defer cancel() 267 err := SendMail(ctx) 268 if assert.Error(t, err) { 269 assert.Equal(t, "yes", err.Error()) 270 } 271 }) 272 273 t.Run("send campaign email", func(t *testing.T) { 274 sendMail = func(_ *job.TaskContext, opts *mail.Options, domain string) error { 275 assert.NotNil(t, opts.From) 276 assert.NotNil(t, opts.To) 277 assert.Len(t, opts.To, 1) 278 assert.Equal(t, "me@me", opts.To[0].Email) 279 assert.Equal(t, "noreply@"+inst.Domain, opts.From.Email) 280 assert.Equal(t, inst.Domain, domain) 281 return errors.New("yes") 282 } 283 defer func() { 284 sendMail = doSendMail 285 }() 286 msg, _ := job.NewMessage(mail.Options{ 287 Mode: mail.ModeCampaign, 288 Subject: "Awesome content", 289 Parts: []*mail.Part{ 290 { 291 Type: "text/plain", 292 Body: "foo", 293 }, 294 }, 295 Locale: "en", 296 }) 297 j := job.NewJob(inst, &job.JobRequest{ 298 Message: msg, 299 WorkerType: "sendmail", 300 }) 301 ctx, cancel := job.NewTaskContext("123", j, inst) 302 defer cancel() 303 err := SendMail(ctx) 304 if assert.Error(t, err) { 305 assert.Equal(t, "yes", err.Error()) 306 } 307 }) 308 } 309 310 func mailServer(t *testing.T, serverString string, clientStrings []string, expectedHeader map[string]string, send func(string, int) error) { 311 serverString = strings.Join(strings.Split(serverString, "\n"), "\r\n") 312 for i, s := range clientStrings { 313 clientStrings[i] = strings.Join(strings.Split(s, "\n"), "\r\n") 314 } 315 316 var cmdbuf bytes.Buffer 317 bcmdbuf := bufio.NewWriter(&cmdbuf) 318 headers := make(map[string]string) 319 l, err := net.Listen("tcp", "127.0.0.1:0") 320 if err != nil { 321 t.Fatalf("Unable to to create listener: %v", err) 322 } 323 defer l.Close() 324 325 // prevent data race on bcmdbuf 326 done := make(chan struct{}) 327 go func(data []string) { 328 defer close(done) 329 330 conn, err := l.Accept() 331 if err != nil { 332 t.Errorf("Accept error: %v", err) 333 return 334 } 335 defer conn.Close() 336 337 tc := textproto.NewConn(conn) 338 readdata := false 339 readhead := false 340 for i := 0; i < len(data) && data[i] != ""; i++ { 341 _ = tc.PrintfLine(data[i]) 342 for len(data[i]) >= 4 && data[i][3] == '-' { 343 i++ 344 _ = tc.PrintfLine(data[i]) 345 } 346 if data[i] == "221 Goodbye" { 347 return 348 } 349 read := false 350 for !read || data[i] == "354 Go ahead" { 351 msg, err := tc.ReadLine() 352 if readdata && msg != "." { 353 if msg == "" { 354 readhead = true 355 read = true 356 continue 357 } 358 // skip multipart --boundaries 359 if readhead && 360 (len(msg) <= 1 || msg[0] != '-' || msg[1] != '-') { 361 _, _ = bcmdbuf.Write([]byte(msg + "\r\n")) 362 } else { 363 parts := strings.SplitN(msg, ": ", 2) 364 if len(parts) == 2 { 365 headers[parts[0]] = parts[1] 366 } 367 } 368 } else { 369 if msg == "." { 370 readdata = false 371 } 372 if msg == "DATA" { 373 readdata = true 374 } 375 _, _ = bcmdbuf.Write([]byte(msg + "\r\n")) 376 read = true 377 } 378 if err != nil { 379 t.Errorf("Read error: %v", err) 380 return 381 } 382 if data[i] == "354 Go ahead" && msg == "." { 383 break 384 } 385 } 386 } 387 }(strings.Split(serverString, "\r\n")) 388 389 host, port, _ := net.SplitHostPort(l.Addr().String()) 390 portI, _ := strconv.Atoi(port) 391 if err := send(host, portI); err != nil { 392 t.Errorf("%v", err) 393 } 394 395 <-done 396 bcmdbuf.Flush() 397 actualcmds := cmdbuf.String() 398 for _, s := range clientStrings { 399 assert.Contains(t, actualcmds, s) 400 } 401 assert.EqualValues(t, expectedHeader, headers) 402 }