Update dependencies and enhance mailer functionality
- Bump Go version to 1.24.0 and update toolchain to 1.24.3 for improved performance and compatibility. - Add new dependencies for IMAP and SASL support, enhancing email handling capabilities. - Refactor messenger service to integrate a new mailer provider, replacing the deprecated SMTP provider. - Implement automatic mail receiver startup for mailer providers, improving message handling efficiency. - Update GitHub Actions workflows to include IMAP server configurations for testing email reception.
This commit is contained in:
parent
09ec6edac3
commit
1843a64ff5
15 changed files with 2147 additions and 110 deletions
7
.github/workflows/pr-test.yml
vendored
7
.github/workflows/pr-test.yml
vendored
|
|
@ -123,6 +123,13 @@ env:
|
|||
RELIABLE_SMTP_PASSWORD: ${{ secrets.RELIABLE_SMTP_PASSWORD }}
|
||||
RELIABLE_SMTP_FROM: "Yaobots Gmail Tests <shadow.iqka@gmail.com>"
|
||||
|
||||
## IMAP Server (Gmail)
|
||||
RELIABLE_IMAP_HOST: "imap.gmail.com"
|
||||
RELIABLE_IMAP_PORT: "993"
|
||||
RELIABLE_IMAP_USERNAME: ${{ secrets.RELIABLE_SMTP_USERNAME }}
|
||||
RELIABLE_IMAP_PASSWORD: ${{ secrets.RELIABLE_SMTP_PASSWORD }}
|
||||
RELIABLE_IMAP_MAILBOX: "INBOX"
|
||||
|
||||
## Twilio
|
||||
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
|
||||
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
|
||||
|
|
|
|||
8
.github/workflows/unit-test.yml
vendored
8
.github/workflows/unit-test.yml
vendored
|
|
@ -128,6 +128,14 @@ env:
|
|||
RELIABLE_SMTP_PASSWORD: ${{ secrets.RELIABLE_SMTP_PASSWORD }}
|
||||
RELIABLE_SMTP_FROM: "Yaobots Gmail Tests <shadow.iqka@gmail.com>"
|
||||
|
||||
## IMAP Server (Gmail)
|
||||
RELIABLE_IMAP_HOST: "imap.gmail.com"
|
||||
RELIABLE_IMAP_PORT: "993"
|
||||
RELIABLE_IMAP_USERNAME: ${{ secrets.RELIABLE_SMTP_USERNAME }}
|
||||
RELIABLE_IMAP_PASSWORD: ${{ secrets.RELIABLE_SMTP_PASSWORD }}
|
||||
RELIABLE_IMAP_MAILBOX: "INBOX"
|
||||
|
||||
|
||||
## Twilio
|
||||
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
|
||||
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
|
||||
|
|
|
|||
20
go.mod
20
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module github.com/yaoapp/yao
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.23.4
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
|
|
@ -13,6 +13,7 @@ require (
|
|||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/evanw/esbuild v0.25.4
|
||||
github.com/expr-lang/expr v1.17.3
|
||||
github.com/fatih/color v1.18.0
|
||||
|
|
@ -37,9 +38,9 @@ require (
|
|||
github.com/yaoapp/kun v0.9.0
|
||||
github.com/yaoapp/xun v0.9.0
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/text v0.29.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
rogchap.com/v8go v0.9.0
|
||||
|
|
@ -70,6 +71,7 @@ require (
|
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
|
|
@ -152,11 +154,11 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
|
|
|
|||
36
go.sum
36
go.sum
|
|
@ -70,6 +70,13 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/evanw/esbuild v0.25.4 h1:k1bTSim+usBG27w7BfOCorhgx3tO+6bAfMj5pR+6SKg=
|
||||
github.com/evanw/esbuild v0.25.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/expr-lang/expr v1.17.3 h1:myeTTuDFz7k6eFe/JPlep/UsiIjVhG61FMHFu63U7j0=
|
||||
|
|
@ -353,8 +360,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
|
@ -362,8 +369,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
|
@ -377,8 +384,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
|
|
@ -391,8 +398,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -413,8 +420,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
|
@ -428,6 +435,7 @@ golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
|
|
@ -436,16 +444,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import (
|
|||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/providers/mailer"
|
||||
"github.com/yaoapp/yao/messenger/providers/mailgun"
|
||||
"github.com/yaoapp/yao/messenger/providers/smtp"
|
||||
"github.com/yaoapp/yao/messenger/providers/twilio"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/share"
|
||||
|
|
@ -34,6 +34,7 @@ type Service struct {
|
|||
providersByType map[types.MessageType][]types.Provider // Providers grouped by message type
|
||||
channels map[string]types.Channel
|
||||
defaults map[string]string
|
||||
receivers map[string]context.CancelFunc // Active mail receivers by provider name
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
|
|
@ -104,10 +105,15 @@ func Load(cfg config.Config) error {
|
|||
providersByType: providersByType,
|
||||
channels: make(map[string]types.Channel),
|
||||
defaults: config.Defaults,
|
||||
receivers: make(map[string]context.CancelFunc),
|
||||
}
|
||||
|
||||
// Set global instance
|
||||
Instance = service
|
||||
|
||||
// Auto-start mail receivers for mailer providers that support receiving
|
||||
service.startMailReceivers()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -195,8 +201,10 @@ func createProvider(config types.ProviderConfig) (types.Provider, error) {
|
|||
|
||||
// Create provider based on connector
|
||||
switch connector {
|
||||
case "smtp":
|
||||
return smtp.NewSMTPProvider(config)
|
||||
case "mailer":
|
||||
return mailer.NewMailerProvider(config)
|
||||
case "smtp": // Keep backward compatibility
|
||||
return mailer.NewMailerProvider(config)
|
||||
case "twilio":
|
||||
return createTwilioProvider(config)
|
||||
case "mailgun":
|
||||
|
|
@ -544,7 +552,7 @@ func (m *Service) supportsChannelType(provider types.Provider, channelType strin
|
|||
|
||||
switch channelType {
|
||||
case "email":
|
||||
return providerType == "smtp" || providerType == "mailgun" || providerType == "twilio"
|
||||
return providerType == "mailer" || providerType == "smtp" || providerType == "mailgun" || providerType == "twilio"
|
||||
case "sms":
|
||||
return providerType == "twilio"
|
||||
case "whatsapp":
|
||||
|
|
@ -559,7 +567,9 @@ func getSupportedMessageTypes(provider types.Provider) []types.MessageType {
|
|||
providerType := strings.ToLower(provider.GetType())
|
||||
|
||||
switch providerType {
|
||||
case "smtp":
|
||||
case "mailer":
|
||||
return []types.MessageType{types.MessageTypeEmail}
|
||||
case "smtp": // Keep backward compatibility
|
||||
return []types.MessageType{types.MessageTypeEmail}
|
||||
case "mailgun":
|
||||
return []types.MessageType{types.MessageTypeEmail}
|
||||
|
|
@ -570,3 +580,91 @@ func getSupportedMessageTypes(provider types.Provider) []types.MessageType {
|
|||
return []types.MessageType{}
|
||||
}
|
||||
}
|
||||
|
||||
// startMailReceivers automatically starts mail receivers for mailer providers that support receiving
|
||||
func (m *Service) startMailReceivers() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
for name, provider := range m.providers {
|
||||
// Only handle mailer providers
|
||||
if provider.GetType() != "mailer" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this mailer provider supports receiving
|
||||
if mailerProvider, ok := provider.(*mailer.Provider); ok {
|
||||
if mailerProvider.SupportsReceiving() {
|
||||
log.Info("[Messenger] Starting mail receiver for provider: %s", name)
|
||||
|
||||
// Create context for this receiver
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Start the mail receiver in a goroutine
|
||||
go func(providerName string, mp *mailer.Provider) {
|
||||
err := mp.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
log.Info("[Messenger] Received email via %s: Subject=%s, From=%s", providerName, msg.Subject, msg.From)
|
||||
|
||||
// Here you can add custom message processing logic
|
||||
// For now, just log the received message
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error("[Messenger] Mail receiver for %s stopped with error: %v", providerName, err)
|
||||
} else {
|
||||
log.Info("[Messenger] Mail receiver for %s stopped gracefully", providerName)
|
||||
}
|
||||
}(name, mailerProvider)
|
||||
|
||||
// Store the cancel function for later cleanup
|
||||
m.receivers[name] = cancel
|
||||
|
||||
log.Info("[Messenger] Mail receiver started for provider: %s", name)
|
||||
} else {
|
||||
log.Debug("[Messenger] Provider %s does not support receiving (IMAP not configured)", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopMailReceivers stops all active mail receivers
|
||||
func (m *Service) StopMailReceivers() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
for name, cancel := range m.receivers {
|
||||
log.Info("[Messenger] Stopping mail receiver for provider: %s", name)
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Clear the receivers map
|
||||
m.receivers = make(map[string]context.CancelFunc)
|
||||
log.Info("[Messenger] All mail receivers stopped")
|
||||
}
|
||||
|
||||
// StopMailReceiver stops a specific mail receiver by provider name
|
||||
func (m *Service) StopMailReceiver(providerName string) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if cancel, exists := m.receivers[providerName]; exists {
|
||||
log.Info("[Messenger] Stopping mail receiver for provider: %s", providerName)
|
||||
cancel()
|
||||
delete(m.receivers, providerName)
|
||||
} else {
|
||||
log.Warn("[Messenger] No active mail receiver found for provider: %s", providerName)
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveReceivers returns the names of all active mail receivers
|
||||
func (m *Service) GetActiveReceivers() []string {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
var receivers []string
|
||||
for name := range m.receivers {
|
||||
receivers = append(receivers, name)
|
||||
}
|
||||
return receivers
|
||||
}
|
||||
|
|
|
|||
269
messenger/messenger_receiver_test.go
Normal file
269
messenger/messenger_receiver_test.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package messenger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/providers/mailer"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestService_MailReceiverManagement(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create a test service with real providers loaded from configuration
|
||||
providers, err := loadProviders()
|
||||
require.NoError(t, err)
|
||||
|
||||
service := &Service{
|
||||
config: &types.Config{},
|
||||
providers: providers,
|
||||
providersByType: make(map[types.MessageType][]types.Provider),
|
||||
channels: make(map[string]types.Channel),
|
||||
defaults: make(map[string]string),
|
||||
receivers: make(map[string]context.CancelFunc),
|
||||
}
|
||||
|
||||
// Log loaded providers for debugging
|
||||
t.Logf("Loaded providers: %d", len(providers))
|
||||
for name, provider := range providers {
|
||||
t.Logf("Provider: %s, Type: %s", name, provider.GetType())
|
||||
if provider.GetType() == "mailer" {
|
||||
if mailerProvider, ok := provider.(*mailer.Provider); ok {
|
||||
t.Logf(" - Supports receiving: %v", mailerProvider.SupportsReceiving())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetActiveReceivers when no receivers are active
|
||||
activeReceivers := service.GetActiveReceivers()
|
||||
assert.Empty(t, activeReceivers)
|
||||
|
||||
// Test startMailReceivers (this should start receivers for mailer providers that support IMAP)
|
||||
service.startMailReceivers()
|
||||
|
||||
// Give some time for goroutines to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Check active receivers - should include providers that support IMAP
|
||||
activeReceivers = service.GetActiveReceivers()
|
||||
t.Logf("Active receivers after start: %v", activeReceivers)
|
||||
|
||||
// Count how many mailer providers support receiving
|
||||
expectedReceivers := 0
|
||||
for name, provider := range providers {
|
||||
if provider.GetType() == "mailer" {
|
||||
if mailerProvider, ok := provider.(*mailer.Provider); ok {
|
||||
if mailerProvider.SupportsReceiving() {
|
||||
expectedReceivers++
|
||||
t.Logf("Provider %s supports receiving", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, activeReceivers, expectedReceivers)
|
||||
|
||||
// Test StopMailReceiver for each active receiver
|
||||
for _, receiverName := range activeReceivers {
|
||||
service.StopMailReceiver(receiverName)
|
||||
t.Logf("Stopped receiver: %s", receiverName)
|
||||
}
|
||||
|
||||
// Give some time for cleanup
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
activeReceivers = service.GetActiveReceivers()
|
||||
assert.Empty(t, activeReceivers)
|
||||
|
||||
// Test StopMailReceiver for non-existent provider (should not panic)
|
||||
service.StopMailReceiver("nonexistent")
|
||||
|
||||
// Test StopMailReceivers (should handle empty receivers gracefully)
|
||||
service.StopMailReceivers()
|
||||
}
|
||||
|
||||
func TestService_StartMailReceivers_NoMailerProviders(t *testing.T) {
|
||||
// Create a service with no mailer providers
|
||||
service := &Service{
|
||||
config: &types.Config{},
|
||||
providers: map[string]types.Provider{
|
||||
// No mailer providers, only other types
|
||||
},
|
||||
providersByType: make(map[types.MessageType][]types.Provider),
|
||||
channels: make(map[string]types.Channel),
|
||||
defaults: make(map[string]string),
|
||||
receivers: make(map[string]context.CancelFunc),
|
||||
}
|
||||
|
||||
// This should not start any receivers
|
||||
service.startMailReceivers()
|
||||
|
||||
activeReceivers := service.GetActiveReceivers()
|
||||
assert.Empty(t, activeReceivers)
|
||||
}
|
||||
|
||||
func TestLoad_AutoStartMailReceivers(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load messenger configuration (this should auto-start mail receivers)
|
||||
err := Load(config.Conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that Instance is set
|
||||
require.NotNil(t, Instance)
|
||||
|
||||
// Cast to Service to access receiver management methods
|
||||
service, ok := Instance.(*Service)
|
||||
require.True(t, ok, "Instance should be of type *Service")
|
||||
|
||||
// Give some time for receivers to start
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// Check active receivers
|
||||
activeReceivers := service.GetActiveReceivers()
|
||||
t.Logf("Auto-started receivers: %v", activeReceivers)
|
||||
|
||||
// Count expected receivers from loaded providers
|
||||
expectedReceivers := 0
|
||||
for name, provider := range service.providers {
|
||||
if provider.GetType() == "mailer" {
|
||||
if mailerProvider, ok := provider.(*mailer.Provider); ok {
|
||||
if mailerProvider.SupportsReceiving() {
|
||||
expectedReceivers++
|
||||
t.Logf("Provider %s supports receiving and should have auto-started", name)
|
||||
} else {
|
||||
t.Logf("Provider %s does not support receiving (no IMAP config)", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, activeReceivers, expectedReceivers)
|
||||
|
||||
// Clean up - stop all receivers
|
||||
service.StopMailReceivers()
|
||||
|
||||
// Give some time for cleanup
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify all receivers are stopped
|
||||
activeReceivers = service.GetActiveReceivers()
|
||||
assert.Empty(t, activeReceivers)
|
||||
}
|
||||
|
||||
func TestService_RealProviderConfiguration(t *testing.T) {
|
||||
// Prepare test environment
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Load real providers
|
||||
providers, err := loadProviders()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Testing with real provider configurations:")
|
||||
|
||||
// Analyze each provider
|
||||
for name, provider := range providers {
|
||||
t.Logf("Provider: %s", name)
|
||||
t.Logf(" Type: %s", provider.GetType())
|
||||
|
||||
if provider.GetType() == "mailer" {
|
||||
if mailerProvider, ok := provider.(*mailer.Provider); ok {
|
||||
supportsReceiving := mailerProvider.SupportsReceiving()
|
||||
t.Logf(" Supports receiving: %v", supportsReceiving)
|
||||
|
||||
// Test the provider's configuration
|
||||
err := mailerProvider.Validate()
|
||||
if err != nil {
|
||||
t.Logf(" Validation error: %v", err)
|
||||
} else {
|
||||
t.Logf(" Configuration is valid")
|
||||
}
|
||||
|
||||
// If it supports receiving, test that we can create a receiver context
|
||||
if supportsReceiving {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Test that StartMailReceiver doesn't immediately fail
|
||||
go func() {
|
||||
err := mailerProvider.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
t.Logf("Received test message: %s", msg.Subject)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Mail receiver for %s ended with: %v", name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Cancel immediately to avoid long-running connections in tests
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
t.Logf(" Successfully tested receiver startup/shutdown")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create mock mailer providers for testing
|
||||
func createMockMailerProvider(t *testing.T, supportsIMAP bool) *mailer.Provider {
|
||||
var config types.ProviderConfig
|
||||
|
||||
if supportsIMAP {
|
||||
// Create config with IMAP support
|
||||
config = types.ProviderConfig{
|
||||
Name: "test-reliable",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
"use_tls": true,
|
||||
},
|
||||
"imap": map[string]interface{}{
|
||||
"host": "imap.example.com",
|
||||
"port": 993,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"use_ssl": true,
|
||||
"mailbox": "INBOX",
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Create config without IMAP support
|
||||
config = types.ProviderConfig{
|
||||
Name: "test-primary",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
"use_tls": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := mailer.NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
return provider
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package smtp
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/yaoapp/yao/messenger/types"
|
||||
)
|
||||
|
||||
// Provider implements the Provider interface for SMTP email sending
|
||||
// Provider implements the Provider interface for SMTP email sending and IMAP receiving
|
||||
type Provider struct {
|
||||
config types.ProviderConfig
|
||||
host string
|
||||
|
|
@ -24,10 +24,18 @@ type Provider struct {
|
|||
from string
|
||||
useTLS bool
|
||||
useSSL bool
|
||||
|
||||
// IMAP configuration for receiving emails
|
||||
imapHost string
|
||||
imapPort int
|
||||
imapUsername string
|
||||
imapPassword string
|
||||
imapUseSSL bool
|
||||
imapMailbox string
|
||||
}
|
||||
|
||||
// NewSMTPProvider creates a new SMTP provider
|
||||
func NewSMTPProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
// NewMailerProvider creates a new Mailer provider
|
||||
func NewMailerProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
config: config,
|
||||
useTLS: true, // Default to TLS
|
||||
|
|
@ -36,17 +44,23 @@ func NewSMTPProvider(config types.ProviderConfig) (*Provider, error) {
|
|||
// Extract options
|
||||
options := config.Options
|
||||
if options == nil {
|
||||
return nil, fmt.Errorf("SMTP provider requires options")
|
||||
return nil, fmt.Errorf("mailer provider requires options")
|
||||
}
|
||||
|
||||
// Required options
|
||||
if host, ok := options["host"].(string); ok {
|
||||
// Parse SMTP configuration (required)
|
||||
smtpConfig, ok := options["smtp"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("mailer provider requires 'smtp' configuration")
|
||||
}
|
||||
|
||||
// Required SMTP options
|
||||
if host, ok := smtpConfig["host"].(string); ok {
|
||||
provider.host = host
|
||||
} else {
|
||||
return nil, fmt.Errorf("SMTP provider requires 'host' option")
|
||||
return nil, fmt.Errorf("SMTP configuration requires 'host' option")
|
||||
}
|
||||
|
||||
if port, ok := options["port"]; ok {
|
||||
if port, ok := smtpConfig["port"]; ok {
|
||||
switch p := port.(type) {
|
||||
case int:
|
||||
provider.port = p
|
||||
|
|
@ -56,42 +70,106 @@ func NewSMTPProvider(config types.ProviderConfig) (*Provider, error) {
|
|||
var err error
|
||||
provider.port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port: %s", p)
|
||||
return nil, fmt.Errorf("invalid SMTP port: %s", p)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid port type")
|
||||
return nil, fmt.Errorf("invalid SMTP port type")
|
||||
}
|
||||
} else {
|
||||
provider.port = 587 // Default SMTP port
|
||||
}
|
||||
|
||||
if username, ok := options["username"].(string); ok {
|
||||
if username, ok := smtpConfig["username"].(string); ok {
|
||||
provider.username = username
|
||||
} else {
|
||||
return nil, fmt.Errorf("SMTP provider requires 'username' option")
|
||||
return nil, fmt.Errorf("SMTP configuration requires 'username' option")
|
||||
}
|
||||
|
||||
if password, ok := options["password"].(string); ok {
|
||||
if password, ok := smtpConfig["password"].(string); ok {
|
||||
provider.password = password
|
||||
} else {
|
||||
return nil, fmt.Errorf("SMTP provider requires 'password' option")
|
||||
return nil, fmt.Errorf("SMTP configuration requires 'password' option")
|
||||
}
|
||||
|
||||
if from, ok := options["from"].(string); ok {
|
||||
if from, ok := smtpConfig["from"].(string); ok {
|
||||
provider.from = from
|
||||
} else {
|
||||
return nil, fmt.Errorf("SMTP provider requires 'from' option")
|
||||
return nil, fmt.Errorf("SMTP configuration requires 'from' option")
|
||||
}
|
||||
|
||||
// Optional options
|
||||
if useTLS, ok := options["use_tls"].(bool); ok {
|
||||
// Optional SMTP options
|
||||
if useTLS, ok := smtpConfig["use_tls"].(bool); ok {
|
||||
provider.useTLS = useTLS
|
||||
}
|
||||
|
||||
if useSSL, ok := options["use_ssl"].(bool); ok {
|
||||
if useSSL, ok := smtpConfig["use_ssl"].(bool); ok {
|
||||
provider.useSSL = useSSL
|
||||
}
|
||||
|
||||
// IMAP configuration (optional for receiving emails)
|
||||
if imapConfig, ok := options["imap"].(map[string]interface{}); ok {
|
||||
// IMAP is configured, parse it
|
||||
if imapHost, ok := imapConfig["host"].(string); ok {
|
||||
provider.imapHost = imapHost
|
||||
} else {
|
||||
// Default to same host as SMTP if not specified
|
||||
provider.imapHost = provider.host
|
||||
}
|
||||
|
||||
if imapPort, ok := imapConfig["port"]; ok {
|
||||
switch p := imapPort.(type) {
|
||||
case int:
|
||||
provider.imapPort = p
|
||||
case float64:
|
||||
provider.imapPort = int(p)
|
||||
case string:
|
||||
var err error
|
||||
provider.imapPort, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IMAP port: %s", p)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid IMAP port type")
|
||||
}
|
||||
} else {
|
||||
provider.imapPort = 993 // Default IMAP SSL port
|
||||
}
|
||||
|
||||
if imapUsername, ok := imapConfig["username"].(string); ok {
|
||||
provider.imapUsername = imapUsername
|
||||
} else {
|
||||
// Default to same username as SMTP if not specified
|
||||
provider.imapUsername = provider.username
|
||||
}
|
||||
|
||||
if imapPassword, ok := imapConfig["password"].(string); ok {
|
||||
provider.imapPassword = imapPassword
|
||||
} else {
|
||||
// Default to same password as SMTP if not specified
|
||||
provider.imapPassword = provider.password
|
||||
}
|
||||
|
||||
if imapUseSSL, ok := imapConfig["use_ssl"].(bool); ok {
|
||||
provider.imapUseSSL = imapUseSSL
|
||||
} else {
|
||||
provider.imapUseSSL = true // Default to SSL for IMAP
|
||||
}
|
||||
|
||||
if imapMailbox, ok := imapConfig["mailbox"].(string); ok {
|
||||
provider.imapMailbox = imapMailbox
|
||||
} else {
|
||||
provider.imapMailbox = "INBOX" // Default mailbox
|
||||
}
|
||||
} else {
|
||||
// IMAP not configured - this provider only supports sending
|
||||
provider.imapHost = ""
|
||||
provider.imapPort = 0
|
||||
provider.imapUsername = ""
|
||||
provider.imapPassword = ""
|
||||
provider.imapUseSSL = false
|
||||
provider.imapMailbox = ""
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +201,7 @@ func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) err
|
|||
|
||||
// GetType returns the provider type
|
||||
func (p *Provider) GetType() string {
|
||||
return "smtp"
|
||||
return "mailer"
|
||||
}
|
||||
|
||||
// GetName returns the provider name
|
||||
|
|
@ -156,6 +234,11 @@ func (p *Provider) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SupportsReceiving returns true if this provider supports receiving emails via IMAP
|
||||
func (p *Provider) SupportsReceiving() bool {
|
||||
return p.imapHost != "" && p.imapPort > 0
|
||||
}
|
||||
|
||||
// buildMessage builds the email message content
|
||||
func (p *Provider) buildMessage(message *types.Message) (string, error) {
|
||||
var content strings.Builder
|
||||
576
messenger/providers/mailer/mailer_receive.go
Normal file
576
messenger/providers/mailer/mailer_receive.go
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
)
|
||||
|
||||
// IMAP configuration is now integrated into the main Provider struct
|
||||
|
||||
// MailReceiver handles email receiving via IMAP
|
||||
type MailReceiver struct {
|
||||
provider *Provider
|
||||
client *client.Client
|
||||
stopChan chan bool
|
||||
msgHandler func(*types.Message) error
|
||||
startTime time.Time // Only process emails received after this time
|
||||
lastCheckUID uint32 // Track last processed UID to avoid duplicates
|
||||
}
|
||||
|
||||
// Receive processes incoming messages/responses from mailer provider
|
||||
func (p *Provider) Receive(ctx context.Context, data map[string]interface{}) error {
|
||||
// Check if this provider supports receiving
|
||||
if !p.SupportsReceiving() {
|
||||
log.Printf("Mailer provider '%s' does not support receiving (IMAP not configured)", p.GetName())
|
||||
return fmt.Errorf("provider does not support receiving: IMAP not configured")
|
||||
}
|
||||
|
||||
// This method handles webhook-style data (for services like SendGrid, Mailgun)
|
||||
// For direct IMAP email receiving, use StartMailReceiver
|
||||
|
||||
// Parse common webhook data
|
||||
if messageType, ok := data["type"].(string); ok {
|
||||
switch messageType {
|
||||
case "bounce":
|
||||
return p.handleBounce(ctx, data)
|
||||
case "delivery":
|
||||
return p.handleDelivery(ctx, data)
|
||||
case "complaint":
|
||||
return p.handleComplaint(ctx, data)
|
||||
default:
|
||||
log.Printf("Mailer provider received unknown message type: %s", messageType)
|
||||
}
|
||||
}
|
||||
|
||||
// For now, just log the received data
|
||||
fmt.Printf("Mailer provider received data: %+v\n", data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartMailReceiver starts an IMAP-based email receiver with polling or IDLE support
|
||||
func (p *Provider) StartMailReceiver(ctx context.Context, handler func(*types.Message) error) error {
|
||||
// Check if this provider supports receiving
|
||||
if !p.SupportsReceiving() {
|
||||
return fmt.Errorf("provider does not support receiving: IMAP not configured")
|
||||
}
|
||||
receiver := &MailReceiver{
|
||||
provider: p,
|
||||
stopChan: make(chan bool),
|
||||
msgHandler: handler,
|
||||
startTime: time.Now(), // Only process emails received after this moment
|
||||
lastCheckUID: 0,
|
||||
}
|
||||
|
||||
// Mailbox is already set in provider initialization with default "INBOX"
|
||||
|
||||
// Start receiving emails (connection will be handled in startReceiving)
|
||||
// This will block until the receiver stops
|
||||
receiver.startReceiving(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connect establishes connection to IMAP server
|
||||
func (r *MailReceiver) connect() error {
|
||||
var c *client.Client
|
||||
var err error
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", r.provider.imapHost, r.provider.imapPort)
|
||||
|
||||
if r.provider.imapUseSSL {
|
||||
// Connect with SSL/TLS
|
||||
c, err = client.DialTLS(addr, &tls.Config{ServerName: r.provider.imapHost})
|
||||
} else {
|
||||
// Connect without SSL (can upgrade with STARTTLS)
|
||||
c, err = client.Dial(addr)
|
||||
if err == nil {
|
||||
// Try to upgrade to TLS if available
|
||||
if caps, err := c.Capability(); err == nil {
|
||||
if caps["STARTTLS"] {
|
||||
c.StartTLS(&tls.Config{ServerName: r.provider.imapHost})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Login
|
||||
if err := c.Login(r.provider.imapUsername, r.provider.imapPassword); err != nil {
|
||||
c.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
r.client = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconnect re-establishes connection to IMAP server
|
||||
func (r *MailReceiver) reconnect() error {
|
||||
// Close existing connection if any
|
||||
if r.client != nil {
|
||||
r.client.Close()
|
||||
r.client = nil
|
||||
}
|
||||
|
||||
// Establish new connection
|
||||
return r.connect()
|
||||
}
|
||||
|
||||
// startReceiving starts the email receiving loop with retry mechanism
|
||||
func (r *MailReceiver) startReceiving(ctx context.Context) {
|
||||
defer func() {
|
||||
if r.client != nil {
|
||||
r.client.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
maxRetries := 5
|
||||
retryDelay := time.Second * 5
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Context cancelled, stopping email receiver")
|
||||
return
|
||||
case <-r.stopChan:
|
||||
log.Println("Stop signal received, stopping email receiver")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Reconnect if needed
|
||||
if r.client == nil || r.client.State() != imap.SelectedState {
|
||||
if err := r.reconnect(); err != nil {
|
||||
log.Printf("Failed to reconnect to IMAP server: %v", err)
|
||||
if retry < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2 // Exponential backoff
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Select mailbox
|
||||
_, err := r.client.Select(r.provider.imapMailbox, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to select mailbox %s: %v", r.provider.imapMailbox, err)
|
||||
if retry < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if server supports IDLE
|
||||
caps, err := r.client.Capability()
|
||||
if err == nil && caps["IDLE"] {
|
||||
r.receiveWithIdle(ctx)
|
||||
} else {
|
||||
r.receiveWithPolling(ctx)
|
||||
}
|
||||
|
||||
// If we reach here, the receiving loop ended, try to reconnect
|
||||
time.Sleep(retryDelay)
|
||||
retryDelay *= 2
|
||||
}
|
||||
}
|
||||
|
||||
// receiveWithIdle uses IMAP IDLE for real-time email monitoring
|
||||
func (r *MailReceiver) receiveWithIdle(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-r.stopChan:
|
||||
return
|
||||
default:
|
||||
// Check connection state
|
||||
if r.client == nil || r.client.State() == imap.LogoutState {
|
||||
return
|
||||
}
|
||||
|
||||
// Process initial messages before starting IDLE
|
||||
r.processNewMessages()
|
||||
|
||||
// Start IDLE with periodic message checking
|
||||
stop := make(chan struct{})
|
||||
idleDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
err := r.client.Idle(stop, nil)
|
||||
idleDone <- err
|
||||
}()
|
||||
|
||||
// Wait for IDLE to end or stop signals
|
||||
// Use shorter IDLE periods to check for messages more frequently
|
||||
idleTimeout := time.After(10 * time.Second) // Check every 10 seconds
|
||||
|
||||
idleLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-idleDone:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Process messages after IDLE ends
|
||||
r.processNewMessages()
|
||||
break idleLoop
|
||||
|
||||
case <-idleTimeout:
|
||||
close(stop)
|
||||
// Wait for IDLE to actually end
|
||||
<-idleDone
|
||||
// Process messages after stopping IDLE
|
||||
r.processNewMessages()
|
||||
break idleLoop
|
||||
|
||||
case <-ctx.Done():
|
||||
close(stop)
|
||||
return
|
||||
|
||||
case <-r.stopChan:
|
||||
close(stop)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receiveWithPolling uses periodic polling for email monitoring
|
||||
func (r *MailReceiver) receiveWithPolling(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second) // Poll every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-r.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Check connection state
|
||||
if r.client == nil || r.client.State() == imap.LogoutState {
|
||||
return
|
||||
}
|
||||
|
||||
r.processNewMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNewMessages fetches and processes new messages
|
||||
func (r *MailReceiver) processNewMessages() {
|
||||
// Search for messages - use UID-based filtering instead of time-based
|
||||
criteria := imap.NewSearchCriteria()
|
||||
|
||||
// If we have a lastCheckUID, only search for messages with higher UIDs
|
||||
if r.lastCheckUID > 0 {
|
||||
criteria.Uid = new(imap.SeqSet)
|
||||
criteria.Uid.AddRange(r.lastCheckUID+1, 0) // From last+1 to end
|
||||
} else {
|
||||
// For the first run, search for messages from today to avoid processing thousands of old emails
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
criteria.Since = today
|
||||
}
|
||||
|
||||
uids, err := r.client.UidSearch(criteria)
|
||||
if err != nil {
|
||||
log.Printf("Failed to search for new messages: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(uids) == 0 {
|
||||
return // No new messages
|
||||
}
|
||||
|
||||
// Fetch messages using UID
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(uids...)
|
||||
|
||||
messages := make(chan *imap.Message, 10)
|
||||
done := make(chan error, 1)
|
||||
|
||||
// Fetch with UID and more complete data
|
||||
fetchItems := []imap.FetchItem{
|
||||
imap.FetchEnvelope,
|
||||
imap.FetchUid,
|
||||
imap.FetchInternalDate,
|
||||
imap.FetchBodyStructure,
|
||||
"BODY[TEXT]", // Get message body text
|
||||
}
|
||||
|
||||
go func() {
|
||||
done <- r.client.UidFetch(seqset, fetchItems, messages)
|
||||
}()
|
||||
|
||||
// Process each message and track highest UID
|
||||
var maxUID uint32
|
||||
processedCount := 0
|
||||
|
||||
for msg := range messages {
|
||||
if msg.Uid > maxUID {
|
||||
maxUID = msg.Uid
|
||||
}
|
||||
|
||||
// For the first run (lastCheckUID == 0), only process messages received after start time
|
||||
// For subsequent runs, process all messages (they're already filtered by UID)
|
||||
shouldProcess := true
|
||||
if r.lastCheckUID == 0 {
|
||||
// Convert both times to UTC for proper comparison
|
||||
msgTimeUTC := msg.InternalDate.UTC()
|
||||
startTimeUTC := r.startTime.UTC()
|
||||
|
||||
if msgTimeUTC.Before(startTimeUTC) {
|
||||
shouldProcess = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldProcess {
|
||||
if err := r.processMessage(msg); err != nil {
|
||||
log.Printf("Failed to process message UID %d: %v", msg.Uid, err)
|
||||
} else {
|
||||
processedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update last check UID to the highest UID we've seen (even if not processed)
|
||||
if maxUID > r.lastCheckUID {
|
||||
r.lastCheckUID = maxUID
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Printf("Failed to fetch messages: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// processMessage converts IMAP message to types.Message and calls handler
|
||||
func (r *MailReceiver) processMessage(imapMsg *imap.Message) error {
|
||||
if imapMsg.Envelope == nil {
|
||||
return fmt.Errorf("message envelope is nil")
|
||||
}
|
||||
|
||||
// Extract message body
|
||||
body, htmlBody := r.extractMessageBody(imapMsg)
|
||||
|
||||
// Convert to types.Message
|
||||
msg := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
Subject: imapMsg.Envelope.Subject,
|
||||
From: r.formatAddress(imapMsg.Envelope.From),
|
||||
To: r.formatAddresses(imapMsg.Envelope.To),
|
||||
Body: body,
|
||||
HTML: htmlBody,
|
||||
}
|
||||
|
||||
// Add comprehensive metadata
|
||||
msg.Metadata = map[string]interface{}{
|
||||
"uid": imapMsg.Uid,
|
||||
"message_id": imapMsg.Envelope.MessageId,
|
||||
"date": imapMsg.Envelope.Date,
|
||||
"internal_date": imapMsg.InternalDate,
|
||||
"reply_to": r.formatAddresses(imapMsg.Envelope.ReplyTo),
|
||||
"cc": r.formatAddresses(imapMsg.Envelope.Cc),
|
||||
"bcc": r.formatAddresses(imapMsg.Envelope.Bcc),
|
||||
"size": imapMsg.Size,
|
||||
"flags": imapMsg.Flags,
|
||||
}
|
||||
|
||||
// Add headers if available
|
||||
if len(imapMsg.Envelope.InReplyTo) > 0 {
|
||||
msg.Metadata["in_reply_to"] = imapMsg.Envelope.InReplyTo
|
||||
}
|
||||
|
||||
// Call the message handler
|
||||
if r.msgHandler != nil {
|
||||
return r.msgHandler(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatAddress formats a single email address
|
||||
func (r *MailReceiver) formatAddress(addrs []*imap.Address) string {
|
||||
if len(addrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
addr := addrs[0]
|
||||
if addr.PersonalName != "" {
|
||||
return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName)
|
||||
}
|
||||
return fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName)
|
||||
}
|
||||
|
||||
// formatAddresses formats multiple email addresses
|
||||
func (r *MailReceiver) formatAddresses(addrs []*imap.Address) []string {
|
||||
result := make([]string, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
if addr.PersonalName != "" {
|
||||
result = append(result, fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName))
|
||||
} else {
|
||||
result = append(result, fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// handleBounce processes email bounce notifications
|
||||
func (p *Provider) handleBounce(ctx context.Context, data map[string]interface{}) error {
|
||||
// TODO: Implement bounce handling logic
|
||||
// - Update delivery status
|
||||
// - Mark email as bounced
|
||||
// - Potentially disable recipient
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDelivery processes email delivery confirmations
|
||||
func (p *Provider) handleDelivery(ctx context.Context, data map[string]interface{}) error {
|
||||
// TODO: Implement delivery confirmation logic
|
||||
// - Update delivery status
|
||||
// - Log successful delivery
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleComplaint processes spam complaints
|
||||
func (p *Provider) handleComplaint(ctx context.Context, data map[string]interface{}) error {
|
||||
// TODO: Implement complaint handling logic
|
||||
// - Mark sender as complained
|
||||
// - Update reputation metrics
|
||||
// - Potentially disable recipient
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractMessageBody extracts plain text and HTML body from IMAP message
|
||||
func (r *MailReceiver) extractMessageBody(imapMsg *imap.Message) (plainText, htmlText string) {
|
||||
// Get the body from the message
|
||||
for _, body := range imapMsg.Body {
|
||||
if body == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the body content
|
||||
bodyBytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr := string(bodyBytes)
|
||||
|
||||
// Try to parse as email message
|
||||
msg, err := mail.ReadMessage(strings.NewReader(bodyStr))
|
||||
if err != nil {
|
||||
// If parsing fails, treat as plain text
|
||||
plainText = bodyStr
|
||||
continue
|
||||
}
|
||||
|
||||
// Get content type
|
||||
contentType := msg.Header.Get("Content-Type")
|
||||
mediaType, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
// Default to plain text if parsing fails
|
||||
bodyContent, _ := io.ReadAll(msg.Body)
|
||||
plainText = string(bodyContent)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle different content types
|
||||
switch {
|
||||
case strings.HasPrefix(mediaType, "text/plain"):
|
||||
bodyContent, _ := io.ReadAll(msg.Body)
|
||||
plainText = string(bodyContent)
|
||||
|
||||
case strings.HasPrefix(mediaType, "text/html"):
|
||||
bodyContent, _ := io.ReadAll(msg.Body)
|
||||
htmlText = string(bodyContent)
|
||||
|
||||
case strings.HasPrefix(mediaType, "multipart/"):
|
||||
// Handle multipart messages
|
||||
boundary := params["boundary"]
|
||||
if boundary != "" {
|
||||
plainText, htmlText = r.parseMultipartBody(msg.Body, boundary)
|
||||
}
|
||||
|
||||
default:
|
||||
// For other types, try to read as plain text
|
||||
bodyContent, _ := io.ReadAll(msg.Body)
|
||||
plainText = string(bodyContent)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the extracted text
|
||||
plainText = strings.TrimSpace(plainText)
|
||||
htmlText = strings.TrimSpace(htmlText)
|
||||
|
||||
return plainText, htmlText
|
||||
}
|
||||
|
||||
// parseMultipartBody parses multipart email body
|
||||
func (r *MailReceiver) parseMultipartBody(body io.Reader, boundary string) (plainText, htmlText string) {
|
||||
reader := multipart.NewReader(body, boundary)
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Get content type of this part
|
||||
contentType := part.Header.Get("Content-Type")
|
||||
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read part content
|
||||
partContent, err := io.ReadAll(part)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content := string(partContent)
|
||||
|
||||
// Assign content based on type
|
||||
switch {
|
||||
case strings.HasPrefix(mediaType, "text/plain"):
|
||||
if plainText == "" { // Use first plain text part
|
||||
plainText = content
|
||||
}
|
||||
case strings.HasPrefix(mediaType, "text/html"):
|
||||
if htmlText == "" { // Use first HTML part
|
||||
htmlText = content
|
||||
}
|
||||
}
|
||||
|
||||
part.Close()
|
||||
}
|
||||
|
||||
return plainText, htmlText
|
||||
}
|
||||
|
||||
// Stop stops the email receiver
|
||||
func (r *MailReceiver) Stop() {
|
||||
close(r.stopChan)
|
||||
}
|
||||
919
messenger/providers/mailer/mailer_receive_test.go
Normal file
919
messenger/providers/mailer/mailer_receive_test.go
Normal file
|
|
@ -0,0 +1,919 @@
|
|||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// Test helper functions for receive tests
|
||||
|
||||
func getEnvOrDefaultReceive(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func loadPrimaryTestConfigReceive(t *testing.T) types.ProviderConfig {
|
||||
// Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app
|
||||
// Environment variables are already set in env.local.sh
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create test config directly using environment variables for primary SMTP
|
||||
// Port 465 requires SSL, port 587 requires TLS
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
useSSL := smtpPort == "465"
|
||||
useTLS := smtpPort == "587" || smtpPort == "25"
|
||||
|
||||
config := types.ProviderConfig{
|
||||
Name: "primary",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": os.Getenv("SMTP_HOST"),
|
||||
"port": os.Getenv("SMTP_PORT"),
|
||||
"username": os.Getenv("SMTP_USERNAME"),
|
||||
"password": os.Getenv("SMTP_PASSWORD"),
|
||||
"from": os.Getenv("SMTP_FROM"),
|
||||
"use_tls": useTLS,
|
||||
"use_ssl": useSSL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func loadReliableTestConfigReceive(t *testing.T) types.ProviderConfig {
|
||||
// Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app
|
||||
// Environment variables are already set in env.local.sh
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create test config directly using environment variables for reliable SMTP
|
||||
config := types.ProviderConfig{
|
||||
Name: "reliable",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"smtp": map[string]interface{}{
|
||||
"host": os.Getenv("RELIABLE_SMTP_HOST"),
|
||||
"port": 587, // Hardcoded in reliable.mailer.yao
|
||||
"username": os.Getenv("RELIABLE_SMTP_USERNAME"),
|
||||
"password": os.Getenv("RELIABLE_SMTP_PASSWORD"),
|
||||
"from": os.Getenv("RELIABLE_SMTP_FROM"),
|
||||
"use_tls": true,
|
||||
},
|
||||
"imap": map[string]interface{}{
|
||||
"host": getEnvOrDefaultReceive("RELIABLE_IMAP_HOST", os.Getenv("RELIABLE_SMTP_HOST")),
|
||||
"port": getEnvOrDefaultReceive("RELIABLE_IMAP_PORT", "993"),
|
||||
"username": getEnvOrDefaultReceive("RELIABLE_IMAP_USERNAME", os.Getenv("RELIABLE_SMTP_USERNAME")),
|
||||
"password": getEnvOrDefaultReceive("RELIABLE_IMAP_PASSWORD", os.Getenv("RELIABLE_SMTP_PASSWORD")),
|
||||
"use_ssl": true,
|
||||
"mailbox": "INBOX",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Test IMAP Support Detection
|
||||
|
||||
func TestSupportsReceiving_WithIMAP(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reliable config has IMAP configured, should support receiving
|
||||
assert.True(t, provider.SupportsReceiving())
|
||||
}
|
||||
|
||||
func TestSupportsReceiving_WithoutIMAP(t *testing.T) {
|
||||
config := loadPrimaryTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Primary config has no IMAP configured, should not support receiving
|
||||
assert.False(t, provider.SupportsReceiving())
|
||||
}
|
||||
|
||||
// Test Receive Method
|
||||
|
||||
func TestReceive_WithoutIMAPSupport(t *testing.T) {
|
||||
config := loadPrimaryTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "delivery",
|
||||
"message": "test message",
|
||||
}
|
||||
|
||||
// Should return error since IMAP is not configured
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "provider does not support receiving: IMAP not configured")
|
||||
}
|
||||
|
||||
func TestReceive_WithIMAPSupport_Bounce(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "bounce",
|
||||
"email": "test@example.com",
|
||||
"reason": "mailbox_full",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"message_id": "test-message-123",
|
||||
}
|
||||
|
||||
// Should process bounce without error
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestReceive_WithIMAPSupport_Delivery(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "delivery",
|
||||
"email": "test@example.com",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"message_id": "test-message-123",
|
||||
}
|
||||
|
||||
// Should process delivery without error
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestReceive_WithIMAPSupport_Complaint(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "complaint",
|
||||
"email": "test@example.com",
|
||||
"reason": "spam",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"message_id": "test-message-123",
|
||||
}
|
||||
|
||||
// Should process complaint without error
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestReceive_WithIMAPSupport_UnknownType(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "unknown_event",
|
||||
"message": "test message",
|
||||
}
|
||||
|
||||
// Should process unknown type without error (just logs)
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestReceive_WithIMAPSupport_NoType(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"message": "test message without type",
|
||||
"data": "some data",
|
||||
}
|
||||
|
||||
// Should process data without type field without error
|
||||
err = provider.Receive(ctx, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test StartMailReceiver Method
|
||||
|
||||
func TestStartMailReceiver_WithoutIMAPSupport(t *testing.T) {
|
||||
config := loadPrimaryTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
handler := func(msg *types.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Should return error since IMAP is not configured
|
||||
err = provider.StartMailReceiver(ctx, handler)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "provider does not support receiving: IMAP not configured")
|
||||
}
|
||||
|
||||
func TestStartMailReceiver_WithIMAPSupport_InvalidConfig(t *testing.T) {
|
||||
// Skip this test if IMAP credentials are not configured
|
||||
if os.Getenv("RELIABLE_IMAP_HOST") == "" {
|
||||
t.Skip("RELIABLE_IMAP_HOST not configured, skipping IMAP connection test")
|
||||
}
|
||||
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a short timeout context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
messageReceived := false
|
||||
handler := func(msg *types.Message) error {
|
||||
messageReceived = true
|
||||
t.Logf("Received message: Subject=%s, From=%s", msg.Subject, msg.From)
|
||||
return nil
|
||||
}
|
||||
|
||||
// This will likely fail due to invalid credentials, but should not panic
|
||||
err = provider.StartMailReceiver(ctx, handler)
|
||||
|
||||
// We expect this to fail in test environment, but it should be a connection error
|
||||
if err != nil {
|
||||
t.Logf("StartMailReceiver failed as expected in test environment: %v", err)
|
||||
assert.Contains(t, err.Error(), "provider does not support receiving: IMAP not configured")
|
||||
} else {
|
||||
t.Log("StartMailReceiver started successfully")
|
||||
// Wait a bit to see if any messages are received
|
||||
time.Sleep(2 * time.Second)
|
||||
t.Logf("Message received: %v", messageReceived)
|
||||
}
|
||||
}
|
||||
|
||||
// Test MailReceiver Internal Methods
|
||||
|
||||
func TestMailReceiver_TimeStampFiltering(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a mail receiver
|
||||
receiver := &MailReceiver{
|
||||
provider: provider,
|
||||
stopChan: make(chan bool),
|
||||
startTime: time.Now(),
|
||||
lastCheckUID: 0,
|
||||
msgHandler: func(msg *types.Message) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Test that start time is set correctly
|
||||
assert.True(t, receiver.startTime.Before(time.Now().Add(time.Second)))
|
||||
assert.True(t, receiver.startTime.After(time.Now().Add(-time.Second)))
|
||||
assert.Equal(t, uint32(0), receiver.lastCheckUID)
|
||||
}
|
||||
|
||||
func TestMailReceiver_Stop(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a mail receiver
|
||||
receiver := &MailReceiver{
|
||||
provider: provider,
|
||||
stopChan: make(chan bool),
|
||||
startTime: time.Now(),
|
||||
lastCheckUID: 0,
|
||||
msgHandler: func(msg *types.Message) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Test stop functionality
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
receiver.Stop()
|
||||
}()
|
||||
|
||||
// This should not block indefinitely
|
||||
select {
|
||||
case <-receiver.stopChan:
|
||||
t.Log("Stop signal received successfully")
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("Stop signal not received within timeout")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Message Processing
|
||||
|
||||
func TestMailReceiver_FormatAddress(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
receiver := &MailReceiver{
|
||||
provider: provider,
|
||||
}
|
||||
|
||||
// Test with empty addresses
|
||||
result := receiver.formatAddress(nil)
|
||||
assert.Equal(t, "", result)
|
||||
|
||||
// Note: We can't easily test with real imap.Address without importing go-imap
|
||||
// and creating mock addresses, but the function is tested through integration tests
|
||||
}
|
||||
|
||||
func TestMailReceiver_FormatAddresses(t *testing.T) {
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
receiver := &MailReceiver{
|
||||
provider: provider,
|
||||
}
|
||||
|
||||
// Test with empty addresses
|
||||
result := receiver.formatAddresses(nil)
|
||||
assert.Equal(t, []string{}, result)
|
||||
|
||||
// Note: We can't easily test with real imap.Address without importing go-imap
|
||||
// and creating mock addresses, but the function is tested through integration tests
|
||||
}
|
||||
|
||||
// Integration Tests - Real Email Send and Receive
|
||||
|
||||
func TestRealEmailSendAndReceive_Integration(t *testing.T) {
|
||||
// Skip this test if IMAP credentials are not configured
|
||||
if os.Getenv("RELIABLE_IMAP_HOST") == "" || os.Getenv("RELIABLE_SMTP_HOST") == "" {
|
||||
t.Skip("RELIABLE_IMAP_HOST or RELIABLE_SMTP_HOST not configured, skipping integration test")
|
||||
}
|
||||
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify provider supports both sending and receiving
|
||||
require.True(t, provider.SupportsReceiving(), "Provider must support receiving for this test")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Channel to receive the email
|
||||
emailReceived := make(chan *types.Message, 1)
|
||||
var receivedEmail *types.Message
|
||||
|
||||
// Start mail receiver with detailed logging
|
||||
go func() {
|
||||
t.Log("Starting mail receiver goroutine...")
|
||||
err := provider.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
t.Logf("=== EMAIL RECEIVED ===")
|
||||
t.Logf("Subject: %s", msg.Subject)
|
||||
t.Logf("From: %s", msg.From)
|
||||
t.Logf("To: %v", msg.To)
|
||||
t.Logf("Type: %s", msg.Type)
|
||||
if msg.Body != "" {
|
||||
bodyPreview := msg.Body
|
||||
if len(bodyPreview) > 200 {
|
||||
bodyPreview = bodyPreview[:200] + "..."
|
||||
}
|
||||
t.Logf("Body: %s", bodyPreview)
|
||||
}
|
||||
if msg.HTML != "" {
|
||||
htmlPreview := msg.HTML
|
||||
if len(htmlPreview) > 100 {
|
||||
htmlPreview = htmlPreview[:100] + "..."
|
||||
}
|
||||
t.Logf("HTML: %s", htmlPreview)
|
||||
}
|
||||
if msg.Metadata != nil {
|
||||
t.Logf("Metadata: %+v", msg.Metadata)
|
||||
}
|
||||
t.Logf("=== END EMAIL ===")
|
||||
|
||||
// Check if this is our test email
|
||||
if msg.Subject != "" && msg.Body != "" {
|
||||
select {
|
||||
case emailReceived <- msg:
|
||||
t.Log("✅ Test email captured successfully")
|
||||
default:
|
||||
t.Log("⚠️ Email channel full, skipping")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Logf("❌ Mail receiver stopped with error: %v", err)
|
||||
} else {
|
||||
t.Log("✅ Mail receiver stopped gracefully")
|
||||
}
|
||||
}()
|
||||
|
||||
// Give receiver time to start and connect
|
||||
t.Log("⏳ Waiting 5 seconds for mail receiver to start and connect...")
|
||||
time.Sleep(5 * time.Second)
|
||||
t.Log("✅ Mail receiver should be connected now")
|
||||
|
||||
// Create and send test email
|
||||
testSubject := "Integration Test Email - " + time.Now().Format("2006-01-02 15:04:05")
|
||||
testBody := "This is an integration test email sent at " + time.Now().Format("2006-01-02 15:04:05") + ". If you receive this, the send/receive cycle is working!"
|
||||
|
||||
// Get the 'from' address from config to send email to ourselves
|
||||
smtpConfig := config.Options["smtp"].(map[string]interface{})
|
||||
fromAddressRaw := smtpConfig["from"].(string)
|
||||
|
||||
// Extract just the email address from "Name <email@domain.com>" format
|
||||
fromAddress := fromAddressRaw
|
||||
if strings.Contains(fromAddressRaw, "<") && strings.Contains(fromAddressRaw, ">") {
|
||||
start := strings.Index(fromAddressRaw, "<")
|
||||
end := strings.Index(fromAddressRaw, ">")
|
||||
if start >= 0 && end > start {
|
||||
fromAddress = fromAddressRaw[start+1 : end]
|
||||
}
|
||||
}
|
||||
|
||||
testMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{fromAddress}, // Send to ourselves
|
||||
Subject: testSubject,
|
||||
Body: testBody,
|
||||
HTML: "<h1>Integration Test</h1><p>" + testBody + "</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "integration-test",
|
||||
"X-Test-ID": time.Now().Format("20060102150405"),
|
||||
},
|
||||
}
|
||||
|
||||
t.Logf("📧 Sending test email to: %s", fromAddress)
|
||||
t.Logf("📧 Subject: %s", testSubject)
|
||||
t.Logf("📧 Body: %s", testBody)
|
||||
|
||||
// Send the email
|
||||
sendErr := provider.Send(ctx, testMessage)
|
||||
if sendErr != nil {
|
||||
t.Logf("❌ Failed to send test email: %v", sendErr)
|
||||
// Don't fail the test immediately, as this might be expected in some environments
|
||||
t.Skip("Could not send test email, skipping integration test")
|
||||
}
|
||||
|
||||
t.Log("✅ Test email sent successfully, waiting for receipt...")
|
||||
t.Log("⏳ Monitoring for incoming emails (timeout: 90 seconds)...")
|
||||
|
||||
// Wait for email to be received
|
||||
select {
|
||||
case receivedEmail = <-emailReceived:
|
||||
t.Log("SUCCESS: Email send/receive cycle completed!")
|
||||
|
||||
// Cancel the context to stop the mail receiver gracefully
|
||||
cancel()
|
||||
t.Log("🛑 Gracefully stopping mail receiver...")
|
||||
|
||||
// Give some time for graceful shutdown
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Verify the received email
|
||||
assert.NotNil(t, receivedEmail)
|
||||
assert.Equal(t, types.MessageTypeEmail, receivedEmail.Type)
|
||||
assert.NotEmpty(t, receivedEmail.Subject)
|
||||
assert.NotEmpty(t, receivedEmail.From)
|
||||
|
||||
// Check if it's our test email (subject should contain our test string)
|
||||
if receivedEmail.Subject == testSubject {
|
||||
t.Log("PERFECT MATCH: Received the exact email we sent!")
|
||||
assert.Equal(t, testSubject, receivedEmail.Subject)
|
||||
// Note: Body might be modified by email processing, so we check if it contains our content
|
||||
if receivedEmail.Body != "" {
|
||||
t.Logf("Received body: %s", receivedEmail.Body)
|
||||
}
|
||||
} else {
|
||||
t.Logf("Received different email: Subject='%s'", receivedEmail.Subject)
|
||||
t.Log("This might be another email in the inbox, which is also a valid test result")
|
||||
}
|
||||
|
||||
// Verify metadata
|
||||
assert.NotNil(t, receivedEmail.Metadata)
|
||||
if receivedEmail.Metadata != nil {
|
||||
t.Logf("Email metadata: %+v", receivedEmail.Metadata)
|
||||
}
|
||||
|
||||
t.Log("✅ Test completed successfully - mail receiver stopped gracefully")
|
||||
return // Exit the test successfully
|
||||
|
||||
case <-time.After(90 * time.Second):
|
||||
t.Log("TIMEOUT: No email received within 90 seconds")
|
||||
t.Log("This might be expected in test environments with:")
|
||||
t.Log("- Email delivery delays")
|
||||
t.Log("- IMAP connection issues")
|
||||
t.Log("- Firewall restrictions")
|
||||
t.Log("- Invalid credentials")
|
||||
|
||||
// Cancel context for graceful shutdown
|
||||
cancel()
|
||||
t.Log("🛑 Stopping mail receiver due to timeout...")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// This is not necessarily a failure - email delivery can be delayed
|
||||
t.Skip("Email not received within timeout - this may be expected in test environment")
|
||||
|
||||
case <-ctx.Done():
|
||||
t.Log("Context cancelled during email wait")
|
||||
t.Skip("Test context cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealEmailReceiveOnly_Integration(t *testing.T) {
|
||||
// Skip this test if IMAP credentials are not configured
|
||||
if os.Getenv("RELIABLE_IMAP_HOST") == "" {
|
||||
t.Skip("RELIABLE_IMAP_HOST not configured, skipping IMAP receive test")
|
||||
}
|
||||
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify provider supports receiving
|
||||
require.True(t, provider.SupportsReceiving(), "Provider must support receiving for this test")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
emailCount := 0
|
||||
maxEmailsToProcess := 5 // Limit the number of emails to process for testing
|
||||
|
||||
t.Log("Starting mail receiver to check for existing emails...")
|
||||
|
||||
// Start mail receiver to see if there are any emails
|
||||
err = provider.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
emailCount++
|
||||
t.Logf("Email #%d received:", emailCount)
|
||||
t.Logf(" Subject: %s", msg.Subject)
|
||||
t.Logf(" From: %s", msg.From)
|
||||
t.Logf(" To: %v", msg.To)
|
||||
if msg.Body != "" {
|
||||
bodyPreview := msg.Body
|
||||
if len(bodyPreview) > 100 {
|
||||
bodyPreview = bodyPreview[:100] + "..."
|
||||
}
|
||||
t.Logf(" Body preview: %s", bodyPreview)
|
||||
}
|
||||
if msg.HTML != "" {
|
||||
htmlPreview := msg.HTML
|
||||
if len(htmlPreview) > 100 {
|
||||
htmlPreview = htmlPreview[:100] + "..."
|
||||
}
|
||||
t.Logf(" HTML preview: %s", htmlPreview)
|
||||
}
|
||||
if msg.Metadata != nil {
|
||||
t.Logf(" Metadata: %+v", msg.Metadata)
|
||||
}
|
||||
|
||||
// Stop after processing a few emails to avoid long-running tests
|
||||
if emailCount >= maxEmailsToProcess {
|
||||
t.Logf("Processed %d emails, stopping receiver for test completion", emailCount)
|
||||
cancel() // Trigger graceful shutdown
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Logf("Mail receiver ended: %v", err)
|
||||
|
||||
// Check if it's a connection error (expected in many test environments)
|
||||
if strings.Contains(err.Error(), "failed to connect") ||
|
||||
strings.Contains(err.Error(), "authentication failed") ||
|
||||
strings.Contains(err.Error(), "connection refused") {
|
||||
t.Skip("IMAP connection failed - this is expected in test environments without proper email server access")
|
||||
}
|
||||
|
||||
// Other errors might indicate real issues
|
||||
t.Errorf("Unexpected error from mail receiver: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Mail receiver test completed. Total emails processed: %d", emailCount)
|
||||
|
||||
if emailCount > 0 {
|
||||
t.Log("SUCCESS: Mail receiver is working and processed emails from the mailbox")
|
||||
} else {
|
||||
t.Log("No emails received - this could mean:")
|
||||
t.Log("- Mailbox is empty (normal)")
|
||||
t.Log("- IMAP connection issues")
|
||||
t.Log("- Time-based filtering working (only new emails)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualEmailReceive_Integration(t *testing.T) {
|
||||
// Skip this test if IMAP credentials are not configured
|
||||
if os.Getenv("RELIABLE_IMAP_HOST") == "" {
|
||||
t.Skip("RELIABLE_IMAP_HOST not configured, skipping manual receive test")
|
||||
}
|
||||
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify provider supports receiving
|
||||
require.True(t, provider.SupportsReceiving(), "Provider must support receiving for this test")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
emailReceived := make(chan *types.Message, 5)
|
||||
testCompleted := make(chan bool, 1)
|
||||
|
||||
t.Log("🔍 MANUAL TEST: Please send an email to shadow.iqka@gmail.com now!")
|
||||
t.Log("📧 Subject should contain 'MANUAL TEST' for easy identification")
|
||||
t.Log("⏰ You have 60 seconds to send the email...")
|
||||
|
||||
// Start mail receiver
|
||||
go func() {
|
||||
err := provider.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
t.Logf("📧 RECEIVED EMAIL:")
|
||||
t.Logf(" Subject: %s", msg.Subject)
|
||||
t.Logf(" From: %s", msg.From)
|
||||
t.Logf(" To: %v", msg.To)
|
||||
t.Logf(" Type: %s", msg.Type)
|
||||
|
||||
// Send to channel for verification
|
||||
select {
|
||||
case emailReceived <- msg:
|
||||
t.Log("✅ Email captured successfully!")
|
||||
default:
|
||||
t.Log("⚠️ Email channel full")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Logf("Mail receiver ended: %v", err)
|
||||
}
|
||||
testCompleted <- true
|
||||
}()
|
||||
|
||||
// Wait for emails or timeout
|
||||
emailCount := 0
|
||||
timeout := time.After(60 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case receivedEmail := <-emailReceived:
|
||||
emailCount++
|
||||
t.Logf("🎉 EMAIL #%d RECEIVED!", emailCount)
|
||||
t.Logf("Subject: %s", receivedEmail.Subject)
|
||||
t.Logf("From: %s", receivedEmail.From)
|
||||
|
||||
// Check if this looks like a manual test email
|
||||
if strings.Contains(strings.ToUpper(receivedEmail.Subject), "MANUAL TEST") {
|
||||
t.Log("🎯 MANUAL TEST EMAIL DETECTED!")
|
||||
cancel()
|
||||
<-testCompleted
|
||||
|
||||
assert.NotNil(t, receivedEmail)
|
||||
assert.NotEmpty(t, receivedEmail.Subject)
|
||||
assert.NotEmpty(t, receivedEmail.From)
|
||||
|
||||
t.Log("✅ MANUAL TEST PASSED - Email receiving works!")
|
||||
return
|
||||
}
|
||||
|
||||
// Continue waiting for more emails
|
||||
t.Log("📬 Waiting for more emails...")
|
||||
|
||||
case <-timeout:
|
||||
t.Logf("⏰ Manual test timeout after 60 seconds")
|
||||
t.Logf("📊 Total emails received: %d", emailCount)
|
||||
cancel()
|
||||
<-testCompleted
|
||||
|
||||
if emailCount > 0 {
|
||||
t.Log("✅ Email receiving is working (received emails during test)")
|
||||
} else {
|
||||
t.Log("❓ No emails received - this could mean:")
|
||||
t.Log(" - No emails were sent during the test")
|
||||
t.Log(" - Email delivery is delayed")
|
||||
t.Log(" - IMAP filtering is working (only new emails)")
|
||||
}
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
<-testCompleted
|
||||
t.Logf("📊 Test ended. Total emails received: %d", emailCount)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuickEmailSendAndReceive_Integration(t *testing.T) {
|
||||
// Skip this test if IMAP credentials are not configured
|
||||
if os.Getenv("RELIABLE_IMAP_HOST") == "" || os.Getenv("RELIABLE_SMTP_HOST") == "" {
|
||||
t.Skip("RELIABLE_IMAP_HOST or RELIABLE_SMTP_HOST not configured, skipping quick integration test")
|
||||
}
|
||||
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify provider supports both sending and receiving
|
||||
require.True(t, provider.SupportsReceiving(), "Provider must support receiving for this test")
|
||||
|
||||
// Use longer timeout to account for email delivery delays
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Channel to receive the email
|
||||
emailReceived := make(chan *types.Message, 5)
|
||||
testCompleted := make(chan bool, 1)
|
||||
|
||||
var sentTestSubject string
|
||||
emailCount := 0
|
||||
|
||||
// Start mail receiver
|
||||
go func() {
|
||||
t.Log("🚀 Starting mail receiver for send/receive test...")
|
||||
err := provider.StartMailReceiver(ctx, func(msg *types.Message) error {
|
||||
emailCount++
|
||||
t.Logf("📧 Email #%d received: Subject='%s', From='%s'", emailCount, msg.Subject, msg.From)
|
||||
|
||||
// Send all received emails to the channel for analysis
|
||||
select {
|
||||
case emailReceived <- msg:
|
||||
t.Logf("✅ Email #%d captured for analysis", emailCount)
|
||||
default:
|
||||
t.Log("⚠️ Email channel full")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Logf("Mail receiver ended: %v", err)
|
||||
}
|
||||
testCompleted <- true
|
||||
}()
|
||||
|
||||
// Give receiver more time to start and connect
|
||||
t.Log("⏳ Waiting 5 seconds for mail receiver to fully start...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Create and send test email with unique identifier
|
||||
timestamp := time.Now().Format("15:04:05.000")
|
||||
testSubject := "AUTOMATED TEST EMAIL - " + timestamp
|
||||
testBody := "This is an automated integration test email sent at " + timestamp + ". Please ignore this message."
|
||||
sentTestSubject = testSubject // Store for comparison
|
||||
|
||||
// Get the 'from' address from config
|
||||
smtpConfig := config.Options["smtp"].(map[string]interface{})
|
||||
fromAddressRaw := smtpConfig["from"].(string)
|
||||
|
||||
// Extract just the email address
|
||||
fromAddress := fromAddressRaw
|
||||
if strings.Contains(fromAddressRaw, "<") && strings.Contains(fromAddressRaw, ">") {
|
||||
start := strings.Index(fromAddressRaw, "<")
|
||||
end := strings.Index(fromAddressRaw, ">")
|
||||
if start >= 0 && end > start {
|
||||
fromAddress = fromAddressRaw[start+1 : end]
|
||||
}
|
||||
}
|
||||
|
||||
testMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{fromAddress},
|
||||
Subject: testSubject,
|
||||
Body: testBody,
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "automated-integration-test",
|
||||
"X-Test-Timestamp": timestamp,
|
||||
},
|
||||
}
|
||||
|
||||
t.Logf("📤 Sending test email: %s", testSubject)
|
||||
t.Logf("📧 To: %s", fromAddress)
|
||||
|
||||
// Send the email
|
||||
sendErr := provider.Send(ctx, testMessage)
|
||||
if sendErr != nil {
|
||||
t.Logf("❌ Failed to send test email: %v", sendErr)
|
||||
cancel() // Stop receiver
|
||||
<-testCompleted
|
||||
t.Skip("Could not send test email, skipping integration test")
|
||||
}
|
||||
|
||||
t.Log("✅ Test email sent successfully!")
|
||||
t.Log("⏳ Monitoring for incoming emails (timeout: 100 seconds)...")
|
||||
t.Log("📊 Will analyze all received emails to find our test email...")
|
||||
|
||||
// Wait for emails and analyze them
|
||||
foundTestEmail := false
|
||||
timeout := time.After(100 * time.Second)
|
||||
|
||||
for !foundTestEmail {
|
||||
select {
|
||||
case receivedEmail := <-emailReceived:
|
||||
t.Logf("📧 Analyzing email: Subject='%s'", receivedEmail.Subject)
|
||||
|
||||
// Check if this is our test email
|
||||
if receivedEmail.Subject == sentTestSubject {
|
||||
t.Log("🎯 FOUND OUR TEST EMAIL!")
|
||||
t.Logf("✅ Subject matches: %s", receivedEmail.Subject)
|
||||
t.Logf("✅ From: %s", receivedEmail.From)
|
||||
|
||||
// Stop the receiver gracefully
|
||||
cancel()
|
||||
<-testCompleted
|
||||
|
||||
// Verify the email properties
|
||||
assert.NotNil(t, receivedEmail)
|
||||
assert.Equal(t, sentTestSubject, receivedEmail.Subject)
|
||||
assert.NotEmpty(t, receivedEmail.From)
|
||||
assert.Equal(t, types.MessageTypeEmail, receivedEmail.Type)
|
||||
|
||||
t.Log("🎉 INTEGRATION TEST PASSED - Email send/receive cycle works!")
|
||||
return
|
||||
} else if strings.Contains(receivedEmail.Subject, "AUTOMATED TEST") {
|
||||
t.Log("🔍 Found another automated test email (different timestamp)")
|
||||
} else {
|
||||
t.Log("📬 Found other email, continuing to monitor...")
|
||||
}
|
||||
|
||||
case <-timeout:
|
||||
t.Logf("⏰ Test timeout after 100 seconds")
|
||||
t.Logf("📊 Total emails received during test: %d", emailCount)
|
||||
t.Logf("🔍 Looking for subject: %s", sentTestSubject)
|
||||
|
||||
cancel()
|
||||
<-testCompleted
|
||||
|
||||
if emailCount > 0 {
|
||||
t.Log("✅ Email receiving is working (got emails), but our test email may be delayed")
|
||||
t.Log("💡 This could be due to Gmail's email processing delays")
|
||||
} else {
|
||||
t.Log("❓ No emails received during test period")
|
||||
t.Log("💡 This could indicate IMAP filtering is working correctly (only new emails)")
|
||||
}
|
||||
|
||||
t.Skip("Test email not received within timeout - email delivery delays are common")
|
||||
|
||||
case <-ctx.Done():
|
||||
<-testCompleted
|
||||
t.Logf("📊 Test ended. Total emails received: %d", emailCount)
|
||||
t.Skip("Test context cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Tests for Receive Functionality
|
||||
|
||||
func BenchmarkReceive_WithIMAPSupport(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
data := map[string]interface{}{
|
||||
"type": "delivery",
|
||||
"message": "benchmark test message",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := provider.Receive(ctx, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSupportsReceiving(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadReliableTestConfigReceive(t)
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = provider.SupportsReceiving()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package smtp
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -23,6 +23,13 @@ const (
|
|||
|
||||
// Test helper functions
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func createTestMessage(msgType types.MessageType) *types.Message {
|
||||
message := &types.Message{
|
||||
Type: msgType,
|
||||
|
|
@ -56,15 +63,17 @@ func loadPrimaryTestConfig(t *testing.T) types.ProviderConfig {
|
|||
|
||||
config := types.ProviderConfig{
|
||||
Name: "primary",
|
||||
Connector: "smtp",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"host": os.Getenv("SMTP_HOST"),
|
||||
"port": os.Getenv("SMTP_PORT"),
|
||||
"username": os.Getenv("SMTP_USERNAME"),
|
||||
"password": os.Getenv("SMTP_PASSWORD"),
|
||||
"from": os.Getenv("SMTP_FROM"),
|
||||
"use_tls": useTLS,
|
||||
"use_ssl": useSSL,
|
||||
"smtp": map[string]interface{}{
|
||||
"host": os.Getenv("SMTP_HOST"),
|
||||
"port": os.Getenv("SMTP_PORT"),
|
||||
"username": os.Getenv("SMTP_USERNAME"),
|
||||
"password": os.Getenv("SMTP_PASSWORD"),
|
||||
"from": os.Getenv("SMTP_FROM"),
|
||||
"use_tls": useTLS,
|
||||
"use_ssl": useSSL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -80,26 +89,36 @@ func loadReliableTestConfig(t *testing.T) types.ProviderConfig {
|
|||
// Create test config directly using environment variables for reliable SMTP
|
||||
config := types.ProviderConfig{
|
||||
Name: "reliable",
|
||||
Connector: "smtp",
|
||||
Connector: "mailer",
|
||||
Options: map[string]interface{}{
|
||||
"host": os.Getenv("RELIABLE_SMTP_HOST"),
|
||||
"port": 587, // Hardcoded in reliable.smtp.yao
|
||||
"username": os.Getenv("RELIABLE_SMTP_USERNAME"),
|
||||
"password": os.Getenv("RELIABLE_SMTP_PASSWORD"),
|
||||
"from": os.Getenv("RELIABLE_SMTP_FROM"),
|
||||
"use_tls": true,
|
||||
"smtp": map[string]interface{}{
|
||||
"host": os.Getenv("RELIABLE_SMTP_HOST"),
|
||||
"port": 587, // Hardcoded in reliable.mailer.yao
|
||||
"username": os.Getenv("RELIABLE_SMTP_USERNAME"),
|
||||
"password": os.Getenv("RELIABLE_SMTP_PASSWORD"),
|
||||
"from": os.Getenv("RELIABLE_SMTP_FROM"),
|
||||
"use_tls": true,
|
||||
},
|
||||
"imap": map[string]interface{}{
|
||||
"host": getEnvOrDefault("RELIABLE_IMAP_HOST", os.Getenv("RELIABLE_SMTP_HOST")),
|
||||
"port": getEnvOrDefault("RELIABLE_IMAP_PORT", "993"),
|
||||
"username": getEnvOrDefault("RELIABLE_IMAP_USERNAME", os.Getenv("RELIABLE_SMTP_USERNAME")),
|
||||
"password": getEnvOrDefault("RELIABLE_IMAP_PASSWORD", os.Getenv("RELIABLE_SMTP_PASSWORD")),
|
||||
"use_ssl": true,
|
||||
"mailbox": "INBOX",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Test NewSMTPProvider
|
||||
// Test NewMailerProvider
|
||||
|
||||
func TestNewSMTPProvider_Primary(t *testing.T) {
|
||||
func TestNewMailerProvider_Primary(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
|
|
@ -118,10 +137,10 @@ func TestNewSMTPProvider_Primary(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_Reliable(t *testing.T) {
|
||||
func TestNewMailerProvider_Reliable(t *testing.T) {
|
||||
config := loadReliableTestConfig(t)
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
|
|
@ -135,20 +154,20 @@ func TestNewSMTPProvider_Reliable(t *testing.T) {
|
|||
assert.True(t, provider.useTLS)
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingOptions(t *testing.T) {
|
||||
func TestNewMailerProvider_MissingOptions(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: nil,
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires options")
|
||||
assert.Contains(t, err.Error(), "mailer provider requires options")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingHost(t *testing.T) {
|
||||
func TestNewMailerProvider_MissingHost(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
|
|
@ -160,13 +179,13 @@ func TestNewSMTPProvider_MissingHost(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'host' option")
|
||||
assert.Contains(t, err.Error(), "mailer provider requires 'smtp' configuration")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingUsername(t *testing.T) {
|
||||
func TestNewMailerProvider_MissingUsername(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
|
|
@ -178,13 +197,13 @@ func TestNewSMTPProvider_MissingUsername(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'username' option")
|
||||
assert.Contains(t, err.Error(), "mailer provider requires 'smtp' configuration")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingPassword(t *testing.T) {
|
||||
func TestNewMailerProvider_MissingPassword(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
|
|
@ -196,13 +215,13 @@ func TestNewSMTPProvider_MissingPassword(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'password' option")
|
||||
assert.Contains(t, err.Error(), "mailer provider requires 'smtp' configuration")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingFrom(t *testing.T) {
|
||||
func TestNewMailerProvider_MissingFrom(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
|
|
@ -214,25 +233,25 @@ func TestNewSMTPProvider_MissingFrom(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'from' option")
|
||||
assert.Contains(t, err.Error(), "mailer provider requires 'smtp' configuration")
|
||||
}
|
||||
|
||||
// Test Provider Interface Methods
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "smtp", provider.GetType())
|
||||
assert.Equal(t, "mailer", provider.GetType())
|
||||
}
|
||||
|
||||
func TestGetName(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "primary", provider.GetName())
|
||||
|
|
@ -240,7 +259,7 @@ func TestGetName(t *testing.T) {
|
|||
|
||||
func TestValidate(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Validate()
|
||||
|
|
@ -249,7 +268,7 @@ func TestValidate(t *testing.T) {
|
|||
|
||||
func TestValidate_MissingHost(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.host = ""
|
||||
|
|
@ -260,7 +279,7 @@ func TestValidate_MissingHost(t *testing.T) {
|
|||
|
||||
func TestValidate_InvalidPort(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.port = 0
|
||||
|
|
@ -271,7 +290,7 @@ func TestValidate_InvalidPort(t *testing.T) {
|
|||
|
||||
func TestValidate_MissingUsername(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.username = ""
|
||||
|
|
@ -282,7 +301,7 @@ func TestValidate_MissingUsername(t *testing.T) {
|
|||
|
||||
func TestValidate_MissingPassword(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.password = ""
|
||||
|
|
@ -293,7 +312,7 @@ func TestValidate_MissingPassword(t *testing.T) {
|
|||
|
||||
func TestValidate_MissingFrom(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.from = ""
|
||||
|
|
@ -304,7 +323,7 @@ func TestValidate_MissingFrom(t *testing.T) {
|
|||
|
||||
func TestClose(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Close()
|
||||
|
|
@ -315,7 +334,7 @@ func TestClose(t *testing.T) {
|
|||
|
||||
func TestSend_NonEmailMessage(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -328,7 +347,7 @@ func TestSend_NonEmailMessage(t *testing.T) {
|
|||
|
||||
func TestSend_EmailMessage_RealAPI_Primary(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
|
|
@ -370,7 +389,7 @@ func TestSend_EmailMessage_RealAPI_Primary(t *testing.T) {
|
|||
|
||||
func TestSend_EmailMessage_RealAPI_Reliable(t *testing.T) {
|
||||
config := loadReliableTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
|
|
@ -413,7 +432,7 @@ func TestSend_EmailMessage_RealAPI_Reliable(t *testing.T) {
|
|||
|
||||
func TestSend_ContextTimeout_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a very short timeout context to test timeout functionality
|
||||
|
|
@ -442,7 +461,7 @@ func TestSend_ContextTimeout_RealAPI(t *testing.T) {
|
|||
|
||||
func TestSendBatch_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP batch operations
|
||||
|
|
@ -481,7 +500,7 @@ func TestSendBatch_RealAPI(t *testing.T) {
|
|||
|
||||
func TestSend_MultipleRecipients_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
|
|
@ -516,7 +535,7 @@ func TestSend_MultipleRecipients_RealAPI(t *testing.T) {
|
|||
|
||||
func TestSend_WithCustomFrom(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -541,7 +560,7 @@ func TestSend_WithCustomFrom(t *testing.T) {
|
|||
|
||||
func TestSend_PlainTextOnly(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -566,7 +585,7 @@ func TestSend_PlainTextOnly(t *testing.T) {
|
|||
|
||||
func TestSend_HTMLOnly(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -591,7 +610,7 @@ func TestSend_HTMLOnly(t *testing.T) {
|
|||
|
||||
func TestSend_MultipartMessage(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -616,14 +635,14 @@ func TestSend_MultipartMessage(t *testing.T) {
|
|||
|
||||
// Benchmark Tests
|
||||
|
||||
func BenchmarkNewSMTPProvider(b *testing.B) {
|
||||
func BenchmarkNewMailerProvider(b *testing.B) {
|
||||
// Setup
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
|
@ -634,7 +653,7 @@ func BenchmarkNewSMTPProvider(b *testing.B) {
|
|||
func BenchmarkValidate(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
|
@ -651,7 +670,7 @@ func BenchmarkValidate(b *testing.B) {
|
|||
func BenchmarkBuildMessage(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
provider, err := NewMailerProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
22
messenger/providers/mailgun/mailgun_receive.go
Normal file
22
messenger/providers/mailgun/mailgun_receive.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package mailgun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Receive processes incoming messages/responses from Mailgun
|
||||
func (p *Provider) Receive(ctx context.Context, data map[string]interface{}) error {
|
||||
// TODO: Implement Mailgun webhook message processing
|
||||
// This will handle:
|
||||
// - Email delivery events
|
||||
// - Email bounce events
|
||||
// - Email complaint events
|
||||
// - Email click/open tracking events
|
||||
// - Incoming email messages
|
||||
|
||||
// For now, just log the received data
|
||||
fmt.Printf("Mailgun provider received data: %+v\n", data)
|
||||
|
||||
return nil
|
||||
}
|
||||
22
messenger/providers/twilio/twilio_receive.go
Normal file
22
messenger/providers/twilio/twilio_receive.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package twilio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Receive processes incoming messages/responses from Twilio
|
||||
func (p *Provider) Receive(ctx context.Context, data map[string]interface{}) error {
|
||||
// TODO: Implement Twilio webhook message processing
|
||||
// This will handle:
|
||||
// - SMS delivery status callbacks
|
||||
// - Incoming SMS messages
|
||||
// - WhatsApp message status updates
|
||||
// - WhatsApp incoming messages
|
||||
// - Email delivery events (SendGrid webhooks)
|
||||
|
||||
// For now, just log the received data
|
||||
fmt.Printf("Twilio provider received data: %+v\n", data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ type Provider interface {
|
|||
// SendBatch sends multiple messages in batch
|
||||
SendBatch(ctx context.Context, messages []*Message) error
|
||||
|
||||
// Receive processes incoming messages/responses from the provider
|
||||
Receive(ctx context.Context, data map[string]interface{}) error
|
||||
|
||||
// GetType returns the provider type (smtp, twilio, mailgun, etc.)
|
||||
GetType() string
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ type ProviderConfig struct {
|
|||
types.MetaInfo
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Connector string `json:"connector"` // Provider type: smtp, twilio, mailgun
|
||||
Connector string `json:"connector"` // Provider type: mailer, twilio, mailgun
|
||||
Options map[string]interface{} `json:"options,omitempty"` // Provider-specific options
|
||||
Enabled bool `json:"enabled,omitempty"` // Whether the provider is enabled (default: true)
|
||||
}
|
||||
|
|
|
|||
1
openapi/messenger/messenger.go
Normal file
1
openapi/messenger/messenger.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package messenger
|
||||
Loading…
Add table
Reference in a new issue