This commit is contained in:
parent
d57b919d74
commit
49ab24b064
@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/db"
|
||||
"git.pcux.cn/xuans/Smail/dto"
|
||||
"git.pcux.cn/xuans/Smail/dto/parsemail"
|
||||
"git.pcux.cn/xuans/Smail/dto/response"
|
||||
"git.pcux.cn/xuans/Smail/db"
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/list"
|
||||
"git.pcux.cn/xuans/Smail/utils/context"
|
||||
@ -31,17 +31,18 @@ type emailListResponse struct {
|
||||
}
|
||||
|
||||
type emilItem struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Datetime string `json:"datetime"`
|
||||
IsRead bool `json:"is_read"`
|
||||
Sender User `json:"sender"`
|
||||
To []User `json:"to"`
|
||||
Dangerous bool `json:"dangerous"`
|
||||
IsTrusted bool `json:"is_trusted"`
|
||||
IsBlacklisted bool `json:"is_blacklisted"`
|
||||
Error string `json:"error"`
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Datetime string `json:"datetime"`
|
||||
IsRead bool `json:"is_read"`
|
||||
Sender User `json:"sender"`
|
||||
To []User `json:"to"`
|
||||
Dangerous bool `json:"dangerous"`
|
||||
IsTrusted bool `json:"is_trusted"`
|
||||
IsBlacklisted bool `json:"is_blacklisted"`
|
||||
Error string `json:"error"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
@ -138,6 +139,7 @@ func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
IsTrusted: isTrusted,
|
||||
IsBlacklisted: isBlacklisted,
|
||||
Error: email.Error.String,
|
||||
Status: email.Status,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -245,32 +245,12 @@ func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
SendUserID: ctx.UserID,
|
||||
SendDate: now,
|
||||
CronSendTime: now,
|
||||
Status: consts.EmailStatusSent,
|
||||
Status: consts.EmailStatusDeliveryPending,
|
||||
CreateTime: now,
|
||||
MsgID: msgID,
|
||||
}
|
||||
|
||||
err = db.Transaction(db.Instance, func(session *xorm.Session) error {
|
||||
candidate := modelEmail
|
||||
candidate.Id = 0
|
||||
if _, insertErr := session.Insert(&candidate); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
if candidate.Id <= 0 {
|
||||
return fmt.Errorf("insert outgoing email returned no identifier")
|
||||
}
|
||||
relation := models.UserEmail{
|
||||
UserID: ctx.UserID,
|
||||
EmailID: candidate.Id,
|
||||
Status: consts.EmailStatusSent,
|
||||
IsRead: 1,
|
||||
}
|
||||
if _, insertErr := session.Insert(&relation); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
modelEmail = candidate
|
||||
return nil
|
||||
})
|
||||
err = persistOutgoingEmail(db.Instance, ctx.UserID, &modelEmail)
|
||||
|
||||
if err != nil || modelEmail.Id <= 0 {
|
||||
if err != nil {
|
||||
@ -282,7 +262,7 @@ func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ??????????????????
|
||||
// 邮件持久化成功后再记录联系人。
|
||||
var contactList []models.UserContact
|
||||
for _, recipient := range append(append(e.To, e.Cc...), e.Bcc...) {
|
||||
if recipient != nil && recipient.EmailAddress != "" {
|
||||
@ -347,6 +327,31 @@ func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
response.NewSuccessResponse(i18n.GetText(ctx.Lang, "succ")).FPrint(w)
|
||||
}
|
||||
|
||||
func persistOutgoingEmail(engine *xorm.Engine, userID int, email *models.Email) error {
|
||||
return db.Transaction(engine, func(session *xorm.Session) error {
|
||||
candidate := *email
|
||||
candidate.Id = 0
|
||||
candidate.Status = consts.EmailStatusDeliveryPending
|
||||
if _, err := session.Insert(&candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if candidate.Id <= 0 {
|
||||
return fmt.Errorf("insert outgoing email returned no identifier")
|
||||
}
|
||||
relation := models.UserEmail{
|
||||
UserID: userID,
|
||||
EmailID: candidate.Id,
|
||||
Status: consts.EmailStatusDeliveryPending,
|
||||
IsRead: 1,
|
||||
}
|
||||
if _, err := session.Insert(&relation); err != nil {
|
||||
return err
|
||||
}
|
||||
*email = candidate
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func decodeAttachment(data string) (string, []byte, error) {
|
||||
if !strings.HasPrefix(data, "data:") {
|
||||
return "", nil, fmt.Errorf("invalid data url")
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -37,29 +40,12 @@ func TestSendTransactionalPersistenceConsistency(t *testing.T) {
|
||||
SendUserID: ctx.UserID,
|
||||
SendDate: now,
|
||||
CronSendTime: now,
|
||||
Status: consts.EmailStatusSent,
|
||||
Status: consts.EmailStatusDeliveryPending,
|
||||
CreateTime: now,
|
||||
MsgID: "msg-123@example.com",
|
||||
}
|
||||
|
||||
err = db.Transaction(engine, func(session *xorm.Session) error {
|
||||
candidate := modelEmail
|
||||
candidate.Id = 0
|
||||
if _, insertErr := session.Insert(&candidate); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
relation := models.UserEmail{
|
||||
UserID: ctx.UserID,
|
||||
EmailID: candidate.Id,
|
||||
Status: consts.EmailStatusSent,
|
||||
IsRead: 1,
|
||||
}
|
||||
if _, insertErr := session.Insert(&relation); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
modelEmail = candidate
|
||||
return nil
|
||||
})
|
||||
err = persistOutgoingEmail(engine, ctx.UserID, &modelEmail)
|
||||
if err != nil {
|
||||
t.Fatalf("insert email transaction failed: %v", err)
|
||||
}
|
||||
@ -69,7 +55,7 @@ func TestSendTransactionalPersistenceConsistency(t *testing.T) {
|
||||
if err != nil || !has {
|
||||
t.Fatalf("email not found in database: %v", err)
|
||||
}
|
||||
if email.Status != consts.EmailStatusSent || email.Type != consts.EmailTypeSend {
|
||||
if email.Status != consts.EmailStatusDeliveryPending || email.Type != consts.EmailTypeSend {
|
||||
t.Fatalf("unexpected email status/type: status=%d, type=%d", email.Status, email.Type)
|
||||
}
|
||||
|
||||
@ -78,7 +64,7 @@ func TestSendTransactionalPersistenceConsistency(t *testing.T) {
|
||||
if err != nil || !has {
|
||||
t.Fatalf("user_email not found: %v", err)
|
||||
}
|
||||
if userEmail.Status != consts.EmailStatusSent {
|
||||
if userEmail.Status != consts.EmailStatusDeliveryPending {
|
||||
t.Fatalf("unexpected user_email status: %d", userEmail.Status)
|
||||
}
|
||||
}
|
||||
@ -106,3 +92,61 @@ func TestSafeAttachmentName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistOutgoingEmailRollsBackRelationFailure(t *testing.T) {
|
||||
engine, err := xorm.NewEngine("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer engine.Close()
|
||||
engine.SetMaxOpenConns(1)
|
||||
if err := engine.Sync2(&models.Email{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
email := models.Email{Subject: "pending", Status: consts.EmailStatusSent}
|
||||
if err := persistOutgoingEmail(engine, 1, &email); err == nil {
|
||||
t.Fatal("missing relation table did not fail")
|
||||
}
|
||||
count, err := engine.Count(&models.Email{})
|
||||
if err != nil || count != 0 || email.Id != 0 {
|
||||
t.Fatalf("rollback failed: count=%d id=%d error=%v", count, email.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboxExposesDeliveryStatus(t *testing.T) {
|
||||
engine, err := xorm.NewEngine("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer engine.Close()
|
||||
engine.SetMaxOpenConns(1)
|
||||
if err := engine.Sync2(&models.Email{}, &models.UserEmail{}, &models.UserTrustedSender{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := db.Instance
|
||||
db.Instance = engine
|
||||
defer func() { db.Instance = previous }()
|
||||
ctx := &context.Context{UserID: 1}
|
||||
email := models.Email{Subject: "pending", Type: consts.EmailTypeSend, Status: consts.EmailStatusSent}
|
||||
if err := persistOutgoingEmail(engine, ctx.UserID, &email); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, status := range []int8{consts.EmailStatusDeliveryPending, consts.EmailStatusSent, consts.EmailStatusFail} {
|
||||
if _, err := engine.ID(email.Id).Cols("status").Update(&models.Email{Status: status}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest("POST", "/api/v1/email/list", strings.NewReader(`{"tag":"{\"type\":1,\"status\":-1}"}`))
|
||||
writer := httptest.NewRecorder()
|
||||
EmailList(ctx, writer, request)
|
||||
var response struct {
|
||||
ErrorNo int `json:"errorNo"`
|
||||
Data emailListResponse `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(writer.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.ErrorNo != 0 || len(response.Data.List) != 1 || response.Data.List[0].Status != status {
|
||||
t.Fatalf("outbox status %d: %s", status, writer.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,8 +283,8 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
|
||||
"object": domain.mxHost + ":25",
|
||||
}).Infof("SMTP 25 端口 STARTTLS 投递失败: %s", err.Error())
|
||||
|
||||
// 再试用587投递
|
||||
err = smtp.SendMailWithTls(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
|
||||
// 587 使用 STARTTLS,只有 465 使用隐式 TLS。
|
||||
err = smtp.SendMail(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data)
|
||||
if err == nil {
|
||||
accepted.Add(int64(len(tos)))
|
||||
return
|
||||
|
||||
@ -37,7 +37,7 @@ const lang = {
|
||||
"only_prefix": "只需要邮箱前缀",
|
||||
"err_email_format": "邮箱地址错误,请检查邮箱格式!",
|
||||
"err_title_must": "标题必填!",
|
||||
"succ_send": "发送成功!",
|
||||
"succ_send": "邮件已提交,正在投递",
|
||||
"outbox": "发件箱",
|
||||
"modify_pwd": "修改密码",
|
||||
"enter_again": "确认密码",
|
||||
|
||||
@ -78,14 +78,18 @@
|
||||
</el-tooltip>
|
||||
|
||||
<template v-if="isOutbox">
|
||||
<el-tag type="success" effect="light" size="small" v-if="!scope.row.error || scope.row.error === ''">
|
||||
<el-tag type="warning" effect="light" size="small" v-if="scope.row.status === 6">
|
||||
待投递
|
||||
</el-tag>
|
||||
<el-tag type="success" effect="light" size="small" v-else-if="scope.row.status === 1 && !scope.row.error">
|
||||
<el-icon><Check /></el-icon> 发送成功
|
||||
</el-tag>
|
||||
<el-tooltip :content="scope.row.error" placement="top" v-else>
|
||||
<el-tooltip :content="scope.row.error || '未知投递错误'" placement="top" v-else-if="scope.row.status === 2 || scope.row.error">
|
||||
<el-tag type="danger" effect="light" size="small" class="clickable-tag" @click.stop="showError(scope.row)">
|
||||
<el-icon><Warning /></el-icon> 发送失败 (查看详情)
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag type="info" effect="light" size="small" v-else>状态未确认</el-tag>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user