diff --git a/go.mod b/go.mod
index 894900d..d18faa3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,34 +1,38 @@
module apigo.cc/gojs/mail
-go 1.18
+go 1.24
require (
- apigo.cc/gojs v0.0.12
+ apigo.cc/gojs v0.0.25
apigo.cc/gojs/console v0.0.2
- apigo.cc/gojs/redis v0.0.2
- github.com/ssgo/u v1.7.13
+ github.com/emersion/go-imap/v2 v2.0.0-beta.5
+ github.com/emersion/go-message v0.18.1
+ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
+ github.com/ssgo/config v1.7.9
+ github.com/ssgo/u v1.7.21
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
- github.com/dlclark/regexp2 v1.11.4 // indirect
- github.com/emersion/go-imap v1.2.1 // indirect
- github.com/fsnotify/fsnotify v1.8.0 // indirect
+ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
+ github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
- github.com/gomodule/redigo v1.9.2 // indirect
- github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
- github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
- github.com/mattn/go-runewidth v0.0.9 // indirect
+ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
+ github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
+ github.com/jhillyerd/enmime v1.3.0 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
- github.com/ssgo/config v1.7.9 // indirect
- github.com/ssgo/log v1.7.7 // indirect
- github.com/ssgo/redis v1.7.7 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.4 // indirect
+ github.com/ssgo/log v1.7.9 // indirect
github.com/ssgo/standard v1.7.7 // indirect
- github.com/ssgo/tool v0.4.28 // indirect
+ github.com/ssgo/tool v0.4.29 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
- golang.org/x/net v0.32.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ golang.org/x/net v0.42.0 // indirect
+ golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
- gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/imap.go b/imap.go
new file mode 100644
index 0000000..64e7069
--- /dev/null
+++ b/imap.go
@@ -0,0 +1,370 @@
+package mail
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "time"
+
+ "apigo.cc/gojs"
+ "apigo.cc/gojs/goja"
+ "github.com/emersion/go-imap/v2"
+ "github.com/emersion/go-imap/v2/imapclient"
+ "github.com/jhillyerd/enmime"
+ "github.com/ssgo/u"
+)
+
+type Mail struct {
+ Uid uint32
+ From []string
+ To []string
+ Cc []string
+ Bcc []string
+ Subject string
+ Date string
+ Text string
+ Html string
+ Attachments []Attachment
+ Embeds []Attachment
+ Size int64
+ Flags []string
+}
+
+// 接收选项
+type RecvOption struct {
+ Tag uint32
+ Mailbox string
+ Unseen bool
+ Read bool
+ Answered bool
+ Flagged bool
+ Deleted bool
+ Since string
+ Before string
+ Limit uint32
+ MarkAsRead bool
+
+ Subject []string
+ Search []string
+ Body []string
+ From []string
+ To []string
+ Cc []string
+ Bcc []string
+ Not []string
+}
+
+type MailboxInfo struct {
+ Name string
+ Delimiter string
+ Attrs []string
+}
+
+type RecvResult struct {
+ List []Mail
+ Tag uint32
+}
+
+type IMAPClient struct {
+ client *imapclient.Client
+}
+
+func (obj *IMAPClient) Close(vm *goja.Runtime) {
+ obj.client.Logout().Wait()
+ obj.client.Close()
+}
+
+func (obj *Mailbox) OpenIMAP(vm *goja.Runtime) (*IMAPClient, error) {
+ logger := gojs.GetLogger(vm)
+
+ // 连接服务器
+ addr := fmt.Sprintf("%s:%d", obj.imapHost, obj.imapPort)
+ imapClient, err := imapclient.DialTLS(addr, nil)
+ if err != nil {
+ logger.Error("failed to connect to IMAP server", "addr", addr, "err", err)
+ return nil, err
+ }
+
+ // 发送客户端标识
+ imapClient.ID(&imap.IDData{
+ Name: conf.ClientName,
+ Version: conf.ClientVersion,
+ Vendor: conf.ClientVendor,
+ SupportURL: conf.ClientSupportURL,
+ OS: conf.ClientOS,
+ })
+
+ // 登录
+ if err := imapClient.Login(obj.username, obj.password).Wait(); err != nil {
+ logger.Error("IMAP login failed", "user", obj.username, "err", err)
+ return nil, err
+ }
+
+ return &IMAPClient{client: imapClient}, nil
+}
+
+func (obj *IMAPClient) ListMailbox(vm *goja.Runtime) ([]MailboxInfo, error) {
+ logger := gojs.GetLogger(vm)
+ mailboxes, err := obj.client.List("", "%", nil).Collect()
+ if err != nil {
+ logger.Error("failed to list mailboxes", "err", err)
+ return nil, err
+ }
+ list := []MailboxInfo{}
+ for _, mbox := range mailboxes {
+ // fmt.Println(u.JsonP(mbox), 111)
+ attrs := []string{}
+ for _, attr := range mbox.Attrs {
+ attrs = append(attrs, string(attr))
+ }
+ list = append(list, MailboxInfo{
+ Name: mbox.Mailbox,
+ Delimiter: string(mbox.Delim),
+ Attrs: attrs,
+ })
+ }
+ return list, nil
+}
+
+func (obj *IMAPClient) Recv(vm *goja.Runtime, opt *RecvOption) (*RecvResult, error) {
+ logger := gojs.GetLogger(vm)
+
+ // 创建默认选项
+ if opt == nil {
+ opt = &RecvOption{
+ Mailbox: "INBOX",
+ Unseen: true,
+ }
+ }
+ rr := &RecvResult{Tag: opt.Tag, List: []Mail{}}
+
+ // 设置默认限制
+ if opt.Limit == 0 {
+ opt.Limit = 20
+ }
+
+ // 选择邮箱
+ mailboxName := opt.Mailbox
+ if mailboxName == "" {
+ mailboxName = "INBOX"
+ }
+
+ // 选择邮箱
+ c := obj.client
+ selectCmd := c.Select(mailboxName, nil)
+ _, err := selectCmd.Wait()
+ if err != nil {
+ logger.Error("failed to select mailbox", "mailbox", mailboxName, "err", err)
+ return rr, err
+ }
+
+ limit := imap.UID(opt.Limit)
+ startUID := imap.UID(opt.Tag) + 1 // 从上一次最大UID+1开始
+ endUID := imap.UID(math.MaxUint32) // 始终使用最大值,确保搜索时能拿到足够的结果
+ criteria := &imap.SearchCriteria{
+ UID: []imap.UIDSet{[]imap.UIDRange{{Start: startUID, Stop: endUID}}},
+ }
+
+ // 添加基本搜索条件
+ if opt.Unseen {
+ criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen)
+ }
+ if opt.Read {
+ criteria.Flag = append(criteria.Flag, imap.FlagSeen)
+ }
+ if opt.Answered {
+ criteria.Flag = append(criteria.Flag, imap.FlagAnswered)
+ }
+ if opt.Flagged {
+ criteria.Flag = append(criteria.Flag, imap.FlagFlagged)
+ }
+ if opt.Deleted {
+ criteria.Flag = append(criteria.Flag, imap.FlagDeleted)
+ }
+
+ // 添加时间范围条件
+ if opt.Since != "" {
+ criteria.Since = u.ParseTime(opt.Since)
+ }
+ if opt.Before != "" {
+ criteria.Before = u.ParseTime(opt.Before)
+ }
+
+ // 添加自定义搜索条件
+ if len(opt.Search) > 0 {
+ criteria.Text = opt.Search
+ }
+ if len(opt.Body) > 0 {
+ criteria.Body = opt.Body
+ }
+
+ if len(opt.Subject) > 0 {
+ for _, word := range opt.Subject {
+ criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
+ Key: "Subject",
+ Value: word,
+ })
+ }
+ }
+ if len(opt.From) > 0 {
+ for _, word := range opt.From {
+ criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
+ Key: "From",
+ Value: word,
+ })
+ }
+ }
+ if len(opt.To) > 0 {
+ for _, word := range opt.To {
+ criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
+ Key: "To",
+ Value: word,
+ })
+ }
+ }
+ if len(opt.Cc) > 0 {
+ for _, word := range opt.Cc {
+ criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
+ Key: "Cc",
+ Value: word,
+ })
+ }
+ }
+ if len(opt.Bcc) > 0 {
+ for _, word := range opt.Bcc {
+ criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
+ Key: "Bcc",
+ Value: word,
+ })
+ }
+ }
+
+ if len(opt.Not) > 0 {
+ criteria.Not = []imap.SearchCriteria{}
+ criteria.Not = append(criteria.Not, imap.SearchCriteria{
+ Text: opt.Not,
+ })
+ }
+
+ r, err := c.UIDSearch(criteria, nil).Wait()
+ if err != nil {
+ logger.Error("failed to search messages", "err", err)
+ return rr, err
+ }
+
+ var uidSet imap.UIDSet
+ maxUID := startUID
+ var uidCount imap.UID
+ if all, ok := r.All.(imap.UIDSet); ok {
+ for _, set := range all {
+ n := set.Stop - set.Start
+ if uidCount+n > limit {
+ set.Stop -= (uidCount + n - limit)
+ }
+ maxUID = set.Stop
+ uidCount += set.Stop - set.Start
+ uidSet = append(uidSet, set)
+ if uidCount >= limit {
+ break
+ }
+ }
+ }
+
+ if len(uidSet) == 0 {
+ return rr, nil
+ }
+ fr, err := c.Fetch(uidSet, &imap.FetchOptions{Envelope: true, BodySection: []*imap.FetchItemBodySection{{}}}).Collect()
+ if err != nil {
+ logger.Error("failed to fetch messages", "err", err)
+ return rr, err
+ }
+
+ for _, msg := range fr {
+ mail := Mail{
+ Uid: uint32(msg.UID),
+ Subject: msg.Envelope.Subject,
+ From: FormatAddressList(msg.Envelope.From),
+ To: FormatAddressList(msg.Envelope.To),
+ Cc: FormatAddressList(msg.Envelope.Cc),
+ Bcc: FormatAddressList(msg.Envelope.Bcc),
+ Date: msg.Envelope.Date.Format(time.DateTime),
+ Size: msg.RFC822Size,
+ Flags: FormatFlags(msg.Flags),
+ }
+
+ // 解析邮件正文和附件
+ // fmt.Println("####0", len(msg.BodySection))
+ for _, section := range msg.BodySection {
+ if section.Section != nil && section.Section.Specifier == imap.PartSpecifierNone {
+ // fmt.Println("####1", len(section.Bytes))
+ env, err := enmime.ReadEnvelope(bytes.NewReader(section.Bytes))
+ if err == nil {
+ mail.Text = env.Text
+ mail.Html = env.HTML
+
+ // 处理附件
+ for _, attach := range env.Attachments {
+ mail.Attachments = append(mail.Attachments, Attachment{
+ Name: attach.FileName,
+ Data: u.Base64(attach.Content),
+ ContentType: attach.ContentType,
+ })
+ }
+
+ // 处理内嵌资源
+ for _, embed := range env.Inlines {
+ mail.Embeds = append(mail.Embeds, Attachment{
+ Name: embed.FileName,
+ Data: u.Base64(embed.Content),
+ ContentType: embed.ContentType,
+ })
+ }
+ } else {
+ logger.Error("failed to read envelope", "err", err)
+ return rr, err
+ }
+ }
+ }
+
+ rr.List = append(rr.List, mail)
+ }
+
+ rr.Tag = uint32(maxUID)
+ return rr, nil
+}
+
+func FormatFlags(flags []imap.Flag) []string {
+ result := make([]string, len(flags))
+ for i, flag := range flags {
+ result[i] = string(flag)
+ }
+ return result
+}
+
+func ParseFlags(flags []string) []imap.Flag {
+ result := make([]imap.Flag, len(flags))
+ for i, flag := range flags {
+ result[i] = imap.Flag(flag)
+ }
+ return result
+}
+
+// 地址格式化
+func FormatAddress(addr *imap.Address) string {
+ if addr == nil {
+ return ""
+ }
+ if addr.Name != "" {
+ return fmt.Sprintf("%s <%s>", addr.Name, addr.Addr())
+ }
+ return addr.Addr()
+}
+
+// 地址列表格式化
+func FormatAddressList(addrs []imap.Address) []string {
+ result := make([]string, len(addrs))
+ for i, addr := range addrs {
+ result[i] = FormatAddress(&addr)
+ }
+ return result
+}
diff --git a/mail.go b/mail.go
new file mode 100644
index 0000000..f06260d
--- /dev/null
+++ b/mail.go
@@ -0,0 +1,65 @@
+package mail
+
+import (
+ "net/mail"
+
+ "apigo.cc/gojs/goja"
+)
+
+type Mailbox struct {
+ smtpHost string
+ smtpPort int
+ imapHost string
+ imapPort int
+ username string
+ password string
+ senderName string
+ senderMail string
+}
+
+type Attachment struct {
+ Path string
+ Name string
+ Data string
+ ContentType string
+}
+
+func (obj *Mailbox) Recv(vm *goja.Runtime, opt *RecvOption) (*RecvResult, error) {
+ c, err := obj.OpenIMAP(vm)
+ if err != nil {
+ return nil, err
+ }
+ defer c.Close(vm)
+ return c.Recv(vm, opt)
+}
+
+func (obj *Mailbox) ParseAddr(addr string) (name, address string, err error) {
+ r, err := mail.ParseAddress(addr)
+ if err != nil {
+ return "", "", err
+ }
+ return r.Name, r.Address, nil
+}
+
+func (obj *Mailbox) ParseAddrList(addrs []string) ([]map[string]string, error) {
+ result := make([]map[string]string, len(addrs))
+ for i, addr := range addrs {
+ name, address, err := obj.ParseAddr(addr)
+ if err != nil {
+ return nil, err
+ }
+ result[i] = map[string]string{
+ "name": name,
+ "address": address,
+ }
+ }
+ return result, nil
+}
+
+func (obj *Mailbox) FormatAddr(name, address string) string {
+ addr := &mail.Address{
+ Name: name,
+ Address: address,
+ }
+ return addr.String()
+}
diff --git a/mail_test.js b/mail_test.js
new file mode 100644
index 0000000..0082b31
--- /dev/null
+++ b/mail_test.js
@@ -0,0 +1,31 @@
+import mail from 'apigo.cc/gojs/mail'
+import co from 'apigo.cc/gojs/console'
+
+// mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件
!!', {
+// cc: ['isstar3s@126.com'],
+// html: true,
+// embeds: [{ name: '1.png', data: 'iVBORw0KGgoAAAANSUhEUgAAABEAAAATCAMAAABBexbDAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAFZaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chle4QcAAABjUExURaOju6yiu6ajtqKlt7ijttenuaCmvcipuOezvKStwbW2yqSgudSsuNyyuaSpvZeasua3uqWjtcSwv9XR6MS80ei0ys3H3audsLKswruqu8qjvOKrxOKtudOwveTe8+i3xMXC2TWaOysAAADFSURBVBjTRc7tssMQEAZgqW8VFBVEmtz/VZ5dOZ2+M368j92BEMzjdecBmSAI/dEEIegdKM8bxLeT5xStqXOUign/4pyDQxYMitMG4xYgtoFooU0q2VQYYIyhmHx9PgmIMSk7bl2poMTWmOQcpJqSUrpyjBJg4AdqLqVk6NbyoUCWWluMh4X7NQSFD7YJYwylQljJwnrH+VVhDisJY51z2A+wsB4Sfth757s6T5i30DdPON/39/s85/3mvSfYIYdsL+ze/wG5Qw7KZj0DQwAAAABJRU5ErkJggg==' }],
+// })
+
+// let imap = mail.openIMAP()
+// let boxes = imap.listMailbox()
+// co.info(boxes)
+// imap.close()
+
+let r = mail.recv({
+ limit: 1,
+ // subject: ['审核'],
+ from: ['测试工程师'],
+ // search: ['腾讯云备案管局审核通过通知'],
+ // not: ['审核'],
+})
+co.info(r)
+
+// r = mail.recv({
+// limit: 5,
+// tag: r.tag,
+// })
+// co.info(r)
+
+
+return true
\ No newline at end of file
diff --git a/plugin.go b/plugin.go
index 065d681..dd5f22a 100644
--- a/plugin.go
+++ b/plugin.go
@@ -1,40 +1,68 @@
-package plugin
+package mail
import (
- "crypto/tls"
- "encoding/base64"
- "io"
+ "runtime"
"strings"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
- "github.com/jaytaylor/html2text"
"github.com/ssgo/config"
+ "github.com/ssgo/log"
"github.com/ssgo/u"
- "gopkg.in/gomail.v2"
)
const pluginName = "mail"
-var defaultObjectName = "default"
+type Config struct {
+ Mailbox map[string]*MailboxConfig
+ ClientName string
+ ClientVersion string
+ ClientOS string
+ ClientVendor string
+ ClientSupportURL string
+}
+
+type MailboxConfig struct {
+ SmtpHost string
+ SmtpPort int
+ ImapHost string
+ ImapPort int
+ Username string
+ Password string
+ SenderName string
+ SenderMail string
+}
+
+var conf = &Config{
+ Mailbox: make(map[string]*MailboxConfig),
+ ClientName: "Apigo Mail Client",
+ ClientVersion: "1.0.0",
+ ClientOS: runtime.GOOS,
+ ClientVendor: "apigo.cc",
+ ClientSupportURL: "https://apigo.cc/gojs/mail",
+}
+
var defaultObject = &DefaultObject{}
var confAes *u.Aes = u.NewAes([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
-var objects = make(map[string]*Object)
+var objects = make(map[string]*Mailbox)
func init() {
tsCode := gojs.MakeTSCode(defaultObject)
gojs.Register("apigo.cc/gojs/"+pluginName, gojs.Module{
ObjectMaker: func(vm *goja.Runtime) gojs.Map {
- configs := make(map[string]*Config)
- config.LoadConfig(pluginName, &configs)
- for name, object := range configs {
- objects[name] = defaultObject.New(*object)
+ config.LoadConfig(pluginName, conf)
+ for name, object := range conf.Mailbox {
+ objects[name] = defaultObject.New(object)
+ }
+ if defaultMailbox, err := defaultObject.Get("default"); err == nil {
+ defaultObject.Mailbox = defaultMailbox
+ } else if len(objects) == 0 {
+ log.DefaultLogger.Warning("no mailbox configured", "example for env.yml", "mail:\n mailbox:\n default:\n username: ****@***.***")
}
- defaultObject.Object = defaultObject.GetInstance("default")
return gojs.ToMap(defaultObject)
},
TsCode: tsCode,
- Desc: "mail api by github.com/ssgo/mail",
+ Desc: "mail api",
SetSSKey: func(key, iv []byte) {
confAes = u.NewAes(key, iv)
},
@@ -42,18 +70,18 @@ func init() {
}
type DefaultObject struct {
- *Object
+ *Mailbox
}
-func (obj *DefaultObject) GetInstance(name string) *Object {
+func (obj *DefaultObject) Get(name string) (*Mailbox, error) {
if o, ok := objects[name]; ok {
- return o
+ return o, nil
} else {
- return obj.Object
+ return nil, gojs.Err("mailbox [" + name + "] not configured")
}
}
-func (obj *DefaultObject) New(conf Config) *Object {
+func (obj *DefaultObject) New(conf *MailboxConfig) *Mailbox {
a := strings.SplitN(conf.Username, "@", 2)
if len(a) == 2 {
if conf.SmtpHost == "" {
@@ -69,8 +97,11 @@ func (obj *DefaultObject) New(conf Config) *Object {
conf.ImapPort = 993
}
}
+ if conf.SenderMail == "" {
+ conf.SenderMail = conf.Username
+ }
- return &Object{
+ return &Mailbox{
smtpHost: conf.SmtpHost,
smtpPort: conf.SmtpPort,
imapHost: conf.ImapHost,
@@ -78,154 +109,6 @@ func (obj *DefaultObject) New(conf Config) *Object {
username: conf.Username,
password: confAes.DecryptUrlBase64ToString(conf.Password),
senderName: conf.SenderName,
+ senderMail: conf.SenderMail,
}
}
-
-type Config struct {
- SmtpHost string
- SmtpPort int
- ImapHost string
- ImapPort int
- Username string
- Password string
- SenderName string
-}
-
-type Object struct {
- smtpHost string
- smtpPort int
- imapHost string
- imapPort int
- username string
- password string
- senderName string
-}
-
-type Attachment struct {
- Path string
- Name string
- Data string
- ContentType string
-}
-
-type SendOption struct {
- Cc []string
- Bcc []string
- Html bool
- Attachments []Attachment
- Embeds []Attachment
-}
-
-func (obj *Object) Send(vm *goja.Runtime, to []string, subject, content string, option *SendOption) error {
- logger := gojs.GetLogger(vm)
-
- msg := gomail.NewMessage()
- if obj.senderName != "" {
- msg.SetHeader("From", msg.FormatAddress(obj.username, obj.senderName))
- } else {
- msg.SetHeader("From", obj.username)
- }
-
- msg.SetHeader("To", to...)
- msg.SetHeader("Subject", subject)
- useHtml := false
- if option != nil {
- if len(option.Cc) > 0 {
- msg.SetHeader("Cc", option.Cc...)
- }
- if len(option.Bcc) > 0 {
- msg.SetHeader("Bcc", option.Bcc...)
- }
- if len(option.Attachments) > 0 {
- for _, attachment := range option.Attachments {
- if attachment.Path != "" {
- if u.FileExists(attachment.Path) {
- msg.Attach(attachment.Path)
- } else {
- logger.Error("mail attachment file not exists", "filename", attachment.Path, "to", to, "subject", subject)
- }
- } else if attachment.Data != "" {
- if attachment.Name == "" {
- attachment.Name = "attachment"
- }
- if attachment.ContentType == "" {
- attachment.ContentType = "text/plain"
- }
- buf, err := base64.StdEncoding.DecodeString(attachment.Data)
- if err != nil {
- buf = []byte(attachment.Data)
- }
-
- msg.Attach(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
- _, err := w.Write(buf)
- return err
- }), gomail.SetHeader(map[string][]string{
- "Content-Type": {attachment.ContentType},
- }))
- }
- }
- }
- if len(option.Embeds) > 0 {
- for _, attachment := range option.Embeds {
- if attachment.Path != "" {
- if u.FileExists(attachment.Path) {
- msg.Attach(attachment.Path)
- } else {
- logger.Error("mail embed file not exists", "filename", attachment.Path, "to", to, "subject", subject)
- }
- } else if attachment.Data != "" {
- if attachment.Name == "" {
- attachment.Name = "image"
- }
- if attachment.ContentType == "" {
- attachment.ContentType = "image/png"
- }
- buf, err := base64.StdEncoding.DecodeString(attachment.Data)
- if err != nil {
- buf = []byte(attachment.Data)
- }
-
- msg.Embed(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
- _, err := w.Write(buf)
- return err
- }), gomail.SetHeader(map[string][]string{
- "Content-Type": {attachment.ContentType},
- "Content-ID": {"<" + attachment.Name + ">"},
- }))
- }
- }
- }
- useHtml = option.Html
- }
- if useHtml {
- msg.SetBody("text/html", content)
- textContent, err := html2text.FromString(content, html2text.Options{TextOnly: true, PrettyTables: true})
- if err != nil {
- logger.Error("convert html to text failed", "err", err, "to", to, "subject", subject, "content", content)
- textContent = content
- }
- msg.AddAlternative("text/plain", textContent)
- } else {
- msg.SetBody("text/plain", content)
- }
-
- conn := gomail.NewDialer(obj.smtpHost, obj.smtpPort, obj.username, obj.password)
- switch obj.smtpPort {
- case 465:
- conn.SSL = true
- case 587:
- conn.TLSConfig = &tls.Config{
- ServerName: obj.smtpHost,
- InsecureSkipVerify: false,
- }
- }
- // conn.Auth = smtp.PlainAuth("", obj.username, obj.password, obj.smtpHost)
-
- err := conn.DialAndSend(msg)
- if err != nil {
- logger.Error("send mail failed", "err", err.Error(), "to", to, "subject", "from", obj.username, subject, "content", content)
- } else {
- logger.Info("send mail success", "to", to, "from", obj.username, "subject", subject)
- }
- return err
-}
diff --git a/plugin_test.go b/plugin_test.go
index 2ff3b55..f2943aa 100644
--- a/plugin_test.go
+++ b/plugin_test.go
@@ -1,16 +1,65 @@
-package plugin_test
+package mail_test
import (
"fmt"
"testing"
"apigo.cc/gojs"
+ _ "apigo.cc/gojs/console"
"github.com/ssgo/u"
)
func TestPlugin(t *testing.T) {
+ // c, err := imapclient.DialTLS("imap.126.com:993", nil)
+ // if err != nil {
+ // log.Fatalf("failed to dial IMAP server: %v", err)
+ // }
+ // defer c.Close()
+
+ // c.ID(&imap.IDData{
+ // Name: "Apigo Mail Client",
+ // Version: "1.0.0",
+ // Vendor: "apigo.cc",
+ // SupportURL: "https://apigo.cc/gojs/mail",
+ // OS: runtime.GOOS,
+ // })
+
+ // if err := c.Login("api_go@126.com", "ZZEQWMBONBXZGVKY").Wait(); err != nil {
+ // log.Fatalf("failed to login: %v", err)
+ // }
+
+ // mailboxes, err := c.List("", "%", nil).Collect()
+ // if err != nil {
+ // log.Fatalf("failed to list mailboxes: %v", err)
+ // }
+ // log.Printf("Found %v mailboxes", len(mailboxes))
+ // for _, mbox := range mailboxes {
+ // log.Printf(" - %v", mbox.Mailbox)
+ // }
+
+ // selectedMbox, err := c.Select("INBOX", nil).Wait()
+ // if err != nil {
+ // log.Fatalf("failed to select INBOX: %v", err)
+ // }
+ // log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
+
+ // if selectedMbox.NumMessages > 0 {
+ // seqSet := imap.SeqSetNum(1)
+ // fetchOptions := &imap.FetchOptions{Envelope: true}
+ // messages, err := c.Fetch(seqSet, fetchOptions).Collect()
+ // if err != nil {
+ // log.Fatalf("failed to fetch first message in INBOX: %v", err)
+ // }
+ // log.Printf("subject of first message in INBOX: %v", messages[0].Envelope.Subject)
+ // }
+
+ // if err := c.Logout().Wait(); err != nil {
+ // log.Fatalf("failed to logout: %v", err)
+ // }
+
+ // return
gojs.ExportForDev()
- r, err := gojs.RunFile("plugin_test.js")
+ r, err := gojs.RunFile("mail_test.js")
if err != nil {
t.Fatal(err)
} else if r != true {
diff --git a/plugin_test.js b/plugin_test.js
deleted file mode 100644
index 7ac81e2..0000000
--- a/plugin_test.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import mail from 'apigo.cc/gojs/mail'
-
-mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件
!!', {
- cc: ['isstar3s@126.com'],
- html: true,
- embeds: [{ name: '1.png', data: 'iVBORw0KGgoAAAANSUhEUgAAABEAAAATCAMAAABBexbDAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAFZaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chle4QcAAABjUExURaOju6yiu6ajtqKlt7ijttenuaCmvcipuOezvKStwbW2yqSgudSsuNyyuaSpvZeasua3uqWjtcSwv9XR6MS80ei0ys3H3audsLKswruqu8qjvOKrxOKtudOwveTe8+i3xMXC2TWaOysAAADFSURBVBjTRc7tssMQEAZgqW8VFBVEmtz/VZ5dOZ2+M368j92BEMzjdecBmSAI/dEEIegdKM8bxLeT5xStqXOUign/4pyDQxYMitMG4xYgtoFooU0q2VQYYIyhmHx9PgmIMSk7bl2poMTWmOQcpJqSUrpyjBJg4AdqLqVk6NbyoUCWWluMh4X7NQSFD7YJYwylQljJwnrH+VVhDisJY51z2A+wsB4Sfth757s6T5i30DdPON/39/s85/3mvSfYIYdsL+ze/wG5Qw7KZj0DQwAAAABJRU5ErkJggg==' }],
-})
-
-return true
\ No newline at end of file
diff --git a/smtp.go b/smtp.go
new file mode 100644
index 0000000..7390d71
--- /dev/null
+++ b/smtp.go
@@ -0,0 +1,135 @@
+package mail
+
+import (
+ "crypto/tls"
+ "encoding/base64"
+ "io"
+
+ "apigo.cc/gojs"
+ "apigo.cc/gojs/goja"
+ "github.com/jaytaylor/html2text"
+ "github.com/ssgo/u"
+ "gopkg.in/gomail.v2"
+)
+
+type SendOption struct {
+ Cc []string
+ Bcc []string
+ Html bool
+ Attachments []Attachment
+ Embeds []Attachment
+}
+
+func (obj *Mailbox) Send(vm *goja.Runtime, to []string, subject, content string, option *SendOption) error {
+ logger := gojs.GetLogger(vm)
+
+ msg := gomail.NewMessage()
+ if obj.senderName != "" {
+ msg.SetHeader("From", msg.FormatAddress(obj.username, obj.senderName))
+ } else {
+ msg.SetHeader("From", obj.username)
+ }
+
+ msg.SetHeader("To", to...)
+ msg.SetHeader("Subject", subject)
+ useHtml := false
+ if option != nil {
+ if len(option.Cc) > 0 {
+ msg.SetHeader("Cc", option.Cc...)
+ }
+ if len(option.Bcc) > 0 {
+ msg.SetHeader("Bcc", option.Bcc...)
+ }
+ if len(option.Attachments) > 0 {
+ for _, attachment := range option.Attachments {
+ if attachment.Path != "" {
+ if u.FileExists(attachment.Path) {
+ msg.Attach(attachment.Path)
+ } else {
+ logger.Error("mail attachment file not exists", "filename", attachment.Path, "to", to, "subject", subject)
+ }
+ } else if attachment.Data != "" {
+ if attachment.Name == "" {
+ attachment.Name = "attachment"
+ }
+ if attachment.ContentType == "" {
+ attachment.ContentType = "text/plain"
+ }
+ buf, err := base64.StdEncoding.DecodeString(attachment.Data)
+ if err != nil {
+ buf = []byte(attachment.Data)
+ }
+
+ msg.Attach(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
+ _, err := w.Write(buf)
+ return err
+ }), gomail.SetHeader(map[string][]string{
+ "Content-Type": {attachment.ContentType},
+ }))
+ }
+ }
+ }
+ if len(option.Embeds) > 0 {
+ for _, attachment := range option.Embeds {
+ if attachment.Path != "" {
+ if u.FileExists(attachment.Path) {
+ msg.Attach(attachment.Path)
+ } else {
+ logger.Error("mail embed file not exists", "filename", attachment.Path, "to", to, "subject", subject)
+ }
+ } else if attachment.Data != "" {
+ if attachment.Name == "" {
+ attachment.Name = "image"
+ }
+ if attachment.ContentType == "" {
+ attachment.ContentType = "image/png"
+ }
+ buf, err := base64.StdEncoding.DecodeString(attachment.Data)
+ if err != nil {
+ buf = []byte(attachment.Data)
+ }
+
+ msg.Embed(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
+ _, err := w.Write(buf)
+ return err
+ }), gomail.SetHeader(map[string][]string{
+ "Content-Type": {attachment.ContentType},
+ "Content-ID": {"<" + attachment.Name + ">"},
+ }))
+ }
+ }
+ }
+ useHtml = option.Html
+ }
+ if useHtml {
+ msg.SetBody("text/html", content)
+ textContent, err := html2text.FromString(content, html2text.Options{TextOnly: true, PrettyTables: true})
+ if err != nil {
+ logger.Error("convert html to text failed", "err", err, "to", to, "subject", subject, "content", content)
+ textContent = content
+ }
+ msg.AddAlternative("text/plain", textContent)
+ } else {
+ msg.SetBody("text/plain", content)
+ }
+
+ conn := gomail.NewDialer(obj.smtpHost, obj.smtpPort, obj.username, obj.password)
+ switch obj.smtpPort {
+ case 465:
+ conn.SSL = true
+ case 587:
+ conn.TLSConfig = &tls.Config{
+ ServerName: obj.smtpHost,
+ InsecureSkipVerify: false,
+ }
+ }
+ // conn.Auth = smtp.PlainAuth("", obj.username, obj.password, obj.smtpHost)
+
+ err := conn.DialAndSend(msg)
+ if err != nil {
+ logger.Error("send mail failed", "err", err.Error(), "to", to, "subject", "from", obj.username, subject, "content", content)
+ } else {
+ logger.Info("send mail success", "to", to, "from", obj.username, "subject", subject)
+ }
+ return err
+}