mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
163 lines
4.4 KiB
Go
163 lines
4.4 KiB
Go
// Copyright (c) 2020 Proton Technologies AG
|
|
//
|
|
// This file is part of ProtonMail Bridge.
|
|
//
|
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package smtp
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
|
)
|
|
|
|
type messageGetter interface {
|
|
GetMessage(string) (*pmapi.Message, error)
|
|
}
|
|
|
|
type sendRecorderValue struct {
|
|
messageID string
|
|
time time.Time
|
|
}
|
|
|
|
type sendRecorder struct {
|
|
lock *sync.RWMutex
|
|
hashes map[string]sendRecorderValue
|
|
}
|
|
|
|
func newSendRecorder() *sendRecorder {
|
|
return &sendRecorder{
|
|
lock: &sync.RWMutex{},
|
|
hashes: map[string]sendRecorderValue{},
|
|
}
|
|
}
|
|
|
|
func (q *sendRecorder) getMessageHash(message *pmapi.Message) string {
|
|
// Outlook Calendar updates has only headers (no body) and thus have always
|
|
// the same hash. If the message is type of calendar, the "is sending"
|
|
// check to avoid potential duplicates is skipped. Duplicates should not
|
|
// be a problem in this case as calendar updates are small.
|
|
contentType := message.Header.Get("Content-Type")
|
|
if strings.HasPrefix(contentType, "text/calendar") {
|
|
return ""
|
|
}
|
|
|
|
h := sha256.New()
|
|
_, _ = h.Write([]byte(message.AddressID + message.Subject))
|
|
if message.Sender != nil {
|
|
_, _ = h.Write([]byte(message.Sender.Address))
|
|
}
|
|
for _, to := range message.ToList {
|
|
_, _ = h.Write([]byte(to.Address))
|
|
}
|
|
for _, to := range message.CCList {
|
|
_, _ = h.Write([]byte(to.Address))
|
|
}
|
|
for _, to := range message.BCCList {
|
|
_, _ = h.Write([]byte(to.Address))
|
|
}
|
|
_, _ = h.Write([]byte(message.Body))
|
|
for _, att := range message.Attachments {
|
|
_, _ = h.Write([]byte(att.Name + att.MIMEType + fmt.Sprintf("%d", att.Size)))
|
|
}
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
func (q *sendRecorder) addMessage(hash string) {
|
|
q.lock.Lock()
|
|
defer q.lock.Unlock()
|
|
|
|
q.deleteExpiredKeys()
|
|
q.hashes[hash] = sendRecorderValue{
|
|
time: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (q *sendRecorder) removeMessage(hash string) {
|
|
q.lock.Lock()
|
|
defer q.lock.Unlock()
|
|
|
|
q.deleteExpiredKeys()
|
|
delete(q.hashes, hash)
|
|
}
|
|
|
|
func (q *sendRecorder) setMessageID(hash, messageID string) {
|
|
q.lock.Lock()
|
|
defer q.lock.Unlock()
|
|
|
|
if val, ok := q.hashes[hash]; ok {
|
|
val.messageID = messageID
|
|
q.hashes[hash] = val
|
|
}
|
|
}
|
|
|
|
func (q *sendRecorder) isSendingOrSent(client messageGetter, hash string) (isSending bool, wasSent bool) {
|
|
q.lock.Lock()
|
|
defer q.lock.Unlock()
|
|
|
|
if hash == "" {
|
|
return false, false
|
|
}
|
|
|
|
q.deleteExpiredKeys()
|
|
value, ok := q.hashes[hash]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// If we have a value but don't yet have a messageID, we are in the process of uploading the draft.
|
|
if value.messageID == "" {
|
|
return true, false
|
|
}
|
|
|
|
message, err := client.GetMessage(value.messageID)
|
|
// Message could be deleted or there could be an internet issue or whatever,
|
|
// so let's assume the message was not sent.
|
|
if err != nil {
|
|
return
|
|
}
|
|
if message.Type == pmapi.MessageTypeDraft {
|
|
// If message is in draft for a long time, let's assume there is
|
|
// some problem and message will not be sent anymore.
|
|
if time.Since(time.Unix(message.Time, 0)).Minutes() > 10 {
|
|
return
|
|
}
|
|
isSending = true
|
|
}
|
|
// MessageTypeInboxAndSent can be when message was sent to myself.
|
|
if message.Type == pmapi.MessageTypeSent || message.Type == pmapi.MessageTypeInboxAndSent {
|
|
wasSent = true
|
|
}
|
|
|
|
return isSending, wasSent
|
|
}
|
|
|
|
func (q *sendRecorder) deleteExpiredKeys() {
|
|
for key, value := range q.hashes {
|
|
// It's hard to find a good expiration time.
|
|
// On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat).
|
|
// On the the other, a user could put the device into sleep mode while sending.
|
|
// Changing the expiration time will always make one of the edge cases worse.
|
|
// But both edge cases are something we don't care much about. Important thing is we don't send the same message many times.
|
|
if time.Since(value.time) > 30*time.Minute {
|
|
delete(q.hashes, key)
|
|
}
|
|
}
|
|
}
|