Smail/backend/services/list/list.go
smail 2df35f3160
Some checks are pending
Security Checks / security (push) Waiting to run
补齐草稿保存恢复与发送流程并修正编辑权限判断
2026-09-24 21:49:22 +08:00

356 lines
11 KiB
Go

package list
import (
"fmt"
"strings"
"git.pcux.cn/xuans/Smail/consts"
"git.pcux.cn/xuans/Smail/db"
"git.pcux.cn/xuans/Smail/dto"
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/utils/context"
"git.pcux.cn/xuans/Smail/utils/query"
. "xorm.io/builder"
)
func GetEmailList(ctx *context.Context, tagInfo dto.SearchTag, keyword string, pop3List bool, offset, limit int) (emailList []*response.EmailResponseData, total int64, err error) {
return getList(ctx, tagInfo, keyword, pop3List, offset, limit)
}
func getList(ctx *context.Context, tagInfo dto.SearchTag, keyword string, pop3List bool, offset, limit int) (emailList []*response.EmailResponseData, total int64, err error) {
querySQL, queryParams := genSQL(ctx, false, tagInfo, keyword, pop3List, offset, limit)
if err := db.Instance.SQL(querySQL, queryParams...).Find(&emailList); err != nil {
return nil, 0, fmt.Errorf("query email list: %w", err)
}
totalSQL, totalParams := genSQL(ctx, true, tagInfo, keyword, pop3List, offset, limit)
var countResult struct {
Total int64 `xorm:"total"`
}
if _, err := db.Instance.SQL(totalSQL, totalParams...).Get(&countResult); err != nil {
return nil, 0, fmt.Errorf("query email count: %w", err)
}
return emailList, countResult.Total, nil
}
func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword string, pop3List bool, offset, limit int) (string, []any) {
sqlParams := []any{ctx.UserID}
sql := "select "
if count {
sql += `count(1) AS total from email e left join user_email ue on e.id=ue.email_id where ue.user_id = ? `
} else if pop3List {
sql += `e.id,e.size from email e left join user_email ue on e.id=ue.email_id where ue.user_id = ? `
} else {
sql += `e.*,ue.is_read,ue.is_deleted,ue.status AS user_status from email e left join user_email ue on e.id=ue.email_id where ue.user_id = ? `
}
if tagInfo.Status != -1 {
switch tagInfo.Status {
case consts.EmailStatusDel:
sql += " and (ue.status =? or ue.group_id=?) "
sqlParams = append(sqlParams, tagInfo.Status, models.Deleted)
case consts.EmailStatusDrafts:
sql += " and (ue.status =? or ue.group_id=?) "
sqlParams = append(sqlParams, tagInfo.Status, models.Drafts)
case consts.EmailStatusJunk:
sql += " and (ue.status =? or ue.group_id=?) "
sqlParams = append(sqlParams, tagInfo.Status, models.Junk)
}
} else if tagInfo.Status == -1 {
if tagInfo.Type != 1 {
sql += " and ue.status = 0"
} else {
// 发件箱不展示已删除的邮件
sql += " and ue.status NOT IN (3, 4) AND e.status != 4"
}
}
if tagInfo.Type == consts.EmailTypeReceive {
sql += " and type =? "
sqlParams = append(sqlParams, tagInfo.Type)
} else if tagInfo.Type == consts.EmailTypeSend {
sql += " and (type =? or ue.group_id=?)"
sqlParams = append(sqlParams, tagInfo.Type, models.Sent)
}
if tagInfo.GroupId != -1 {
if tagInfo.GroupId > 0 {
sql += " and ue.group_id=? "
sqlParams = append(sqlParams, tagInfo.GroupId)
} else if tagInfo.GroupId == 0 && tagInfo.Status == -1 {
sql += " and (ue.group_id=? or ue.group_id=0)"
sqlParams = append(sqlParams, models.INBOX)
}
} else {
sql += " and (ue.group_id=0 or ue.group_id =?) "
sqlParams = append(sqlParams, models.INBOX)
}
if keyword != "" {
sql += " and (subject like ? or text like ? )"
sqlParams = append(sqlParams, "%"+keyword+"%", "%"+keyword+"%")
}
if !count && limit == 0 {
limit = 10
}
if count {
return sql, sqlParams
}
sql += " order by e.id desc"
if limit < 10000 {
sql += fmt.Sprintf(" LIMIT %d OFFSET %d ", limit, offset)
}
return sql, sqlParams
}
type statRes struct {
Total int64
Size int64
}
// Stat 查询邮件总数和大小
func Stat(ctx *context.Context) (int64, int64, error) {
sql := `select count(1) as total,sum(size) as size from email e left join user_email ue on e.id=ue.email_id where ue.user_id = ? and e.type = 0 and ue.status != 3`
var ret statRes
if _, err := db.Instance.SQL(sql, ctx.UserID).Get(&ret); err != nil {
return 0, 0, fmt.Errorf("query mailbox statistics: %w", err)
}
return ret.Total, ret.Size, nil
}
type ImapListReq struct {
UidList []int
Star int
End int
}
func buildIMAPMailboxRelationQuery(ctx *context.Context, groupName string) (string, []any, error) {
if ctx == nil || ctx.UserID <= 0 {
return "", nil, fmt.Errorf("invalid mailbox user")
}
relations := "SELECT * FROM user_email WHERE user_id = ? AND email_id > 0"
params := []any{ctx.UserID}
switch groupName {
case "INBOX":
relations += " and status = ? and group_id = 0"
params = append(params, 0)
case "Sent Messages":
relations += " and status = ? and group_id = 0"
params = append(params, 1)
case "Drafts":
relations += " and status = ? and group_id = 0"
params = append(params, 4)
case "Deleted Messages":
relations += " and status = ? and group_id = 0"
params = append(params, 3)
case "Junk":
relations += " and status = ? and group_id = 0"
params = append(params, 5)
default:
var mailbox models.Group
found, err := db.Instance.Table("group").Where("user_id=? and full_path=?", ctx.UserID, groupName).Get(&mailbox)
if err != nil {
return "", nil, fmt.Errorf("query mailbox: %w", err)
}
if !found || mailbox.ID == 0 {
return "", nil, fmt.Errorf("mailbox not found")
}
relations += " and group_id = ?"
params = append(params, mailbox.ID)
}
return "SELECT mailbox_rel.*, ROW_NUMBER() OVER (ORDER BY mailbox_rel.id) AS serial_number FROM (" + relations + ") mailbox_rel", params, nil
}
func GetUEListByUID(ctx *context.Context, groupName string, star, end int, uidList []int) ([]*response.UserEmailUIDData, error) {
if star < 0 || end < 0 || (end > 0 && star > end) {
return nil, fmt.Errorf("invalid UID range")
}
inner, params, err := buildIMAPMailboxRelationQuery(ctx, groupName)
if err != nil {
return nil, err
}
filters := make([]string, 0, 3)
if len(uidList) > 0 {
placeholders, uidArgs, ok := query.IntList(uidList, 10000)
if !ok {
return nil, fmt.Errorf("invalid UID list")
}
filters = append(filters, fmt.Sprintf("id in (%s)", placeholders))
params = append(params, uidArgs...)
}
if star > 0 {
filters = append(filters, "id >= ?")
params = append(params, star)
}
if end > 0 {
filters = append(filters, "id <= ?")
params = append(params, end)
}
sql := "SELECT * FROM (" + inner + ") mailbox"
if len(filters) > 0 {
sql += " WHERE " + strings.Join(filters, " AND ")
}
sql += " ORDER BY id"
var relations []*response.UserEmailUIDData
if err := db.Instance.SQL(sql, params...).Find(&relations); err != nil {
return nil, fmt.Errorf("query mailbox UID relations: %w", err)
}
return relations, nil
}
func GetMaxUIDByMailbox(ctx *context.Context, groupName string) (int, error) {
inner, params, err := buildIMAPMailboxRelationQuery(ctx, groupName)
if err != nil {
return 0, err
}
var result struct {
MaxUID int `xorm:"max_uid"`
}
if _, err := db.Instance.SQL("SELECT COALESCE(MAX(id), 0) AS max_uid FROM ("+inner+") mailbox", params...).Get(&result); err != nil {
return 0, fmt.Errorf("query mailbox maximum UID: %w", err)
}
return result.MaxUID, nil
}
func getEmailListByUidList(ctx *context.Context, groupName string, req ImapListReq) ([]*response.EmailResponseData, error) {
if req.Star < 0 || req.End < 0 || (req.End > 0 && req.Star > req.End) {
return nil, fmt.Errorf("invalid UID range")
}
inner, params, err := buildIMAPMailboxRelationQuery(ctx, groupName)
if err != nil {
return nil, err
}
sql := "SELECT * FROM (" + inner + ") mailbox WHERE "
if req.Star == 0 && req.End == 0 && len(req.UidList) == 0 {
maxUID, err := GetMaxUIDByMailbox(ctx, groupName)
if err != nil {
return nil, err
}
if maxUID == 0 {
return []*response.EmailResponseData{}, nil
}
sql += "id = ?"
params = append(params, maxUID)
sql += " ORDER BY id"
var relations []*response.UserEmailUIDData
if err := db.Instance.SQL(sql, params...).Find(&relations); err != nil {
return nil, fmt.Errorf("query mailbox wildcard UID: %w", err)
}
return hydrateIMAPEmailResponses(relations)
}
if req.Star > 0 {
sql += "id >= ?"
params = append(params, req.Star)
if req.End > 0 {
sql += " and id <= ?"
params = append(params, req.End)
}
} else {
placeholders, uidArgs, ok := query.IntList(req.UidList, 10000)
if !ok {
return nil, fmt.Errorf("invalid UID list")
}
sql += fmt.Sprintf("id in (%s)", placeholders)
params = append(params, uidArgs...)
}
sql += " ORDER BY id"
var relations []*response.UserEmailUIDData
if err := db.Instance.SQL(sql, params...).Find(&relations); err != nil {
return nil, fmt.Errorf("query mailbox UID list: %w", err)
}
return hydrateIMAPEmailResponses(relations)
}
func GetEmailListByGroup(ctx *context.Context, groupName string, req ImapListReq, uid bool) ([]*response.EmailResponseData, error) {
if len(req.UidList) == 0 && req.Star == 0 && req.End == 0 && !uid {
return nil, nil
}
if uid {
return getEmailListByUidList(ctx, groupName, req)
}
if req.Star < 0 || req.End < 0 || (req.End > 0 && req.Star > req.End) {
return nil, fmt.Errorf("invalid sequence range")
}
inner, params, err := buildIMAPMailboxRelationQuery(ctx, groupName)
if err != nil {
return nil, err
}
sql := "SELECT * FROM (" + inner + ") mailbox WHERE "
if req.Star > 0 {
sql += "serial_number >= ?"
params = append(params, req.Star)
if req.End > 0 {
sql += " and serial_number <= ?"
params = append(params, req.End)
}
} else {
placeholders, sequenceArgs, ok := query.IntList(req.UidList, 10000)
if !ok {
return nil, fmt.Errorf("invalid sequence list")
}
sql += fmt.Sprintf("serial_number in (%s)", placeholders)
params = append(params, sequenceArgs...)
}
sql += " ORDER BY serial_number"
var relations []*response.UserEmailUIDData
if err := db.Instance.SQL(sql, params...).Find(&relations); err != nil {
return nil, fmt.Errorf("query mailbox sequence list: %w", err)
}
return hydrateIMAPEmailResponses(relations)
}
func hydrateIMAPEmailResponses(relations []*response.UserEmailUIDData) ([]*response.EmailResponseData, error) {
result := make([]*response.EmailResponseData, 0, len(relations))
if len(relations) == 0 {
return result, nil
}
emailIDs := make([]int, 0, len(relations))
for _, relation := range relations {
if relation == nil || relation.EmailID <= 0 {
continue
}
emailIDs = append(emailIDs, relation.EmailID)
}
if len(emailIDs) == 0 {
return result, nil
}
var stored []*response.EmailResponseData
if err := db.Instance.Table("email").Select("*").Where(Eq{"id": emailIDs}).Find(&stored); err != nil {
return nil, fmt.Errorf("query mailbox messages: %w", err)
}
messageByID := make(map[int]*response.EmailResponseData, len(stored))
for _, message := range stored {
if message != nil {
messageByID[message.Id] = message
}
}
for _, relation := range relations {
message := messageByID[relation.EmailID]
if message == nil {
continue
}
copyMessage := *message
if copyMessage.Type == consts.EmailTypeReceive {
copyMessage.Bcc = "[]"
}
copyMessage.IsRead = relation.IsRead
copyMessage.IsDeleted = relation.IsDeleted
copyMessage.SerialNumber = relation.SerialNumber
copyMessage.UeId = relation.ID
result = append(result, &copyMessage)
}
return result, nil
}