2022-04-28 17:32:27 +02:00
|
|
|
import {ApprovalStatus, ConversationType, MailFolderType, MailMethod, MAX_ATTACHMENT_SIZE, OperationType, ReplyType} from "../../api/common/TutanotaConstants"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {
|
2021-12-28 13:53:11 +01:00
|
|
|
AccessBlockedError,
|
|
|
|
|
LockedError,
|
|
|
|
|
NotAuthorizedError,
|
|
|
|
|
NotFoundError,
|
|
|
|
|
PayloadTooLargeError,
|
|
|
|
|
PreconditionFailedError,
|
|
|
|
|
TooManyRequestsError,
|
2021-02-03 17:13:38 +01:00
|
|
|
} from "../../api/common/error/RestError"
|
|
|
|
|
import {UserError} from "../../api/main/UserError"
|
|
|
|
|
import {getPasswordStrengthForUser, isSecurePassword, PASSWORD_MIN_SECURE_VALUE} from "../../misc/PasswordUtils"
|
2022-04-28 17:32:27 +02:00
|
|
|
import {cleanMatch, deduplicate, downcast, findAndRemove, getFromMap, neverNull, noOp, ofClass, promiseMap, remove, typedValues} from "@tutao/tutanota-utils"
|
2021-12-28 13:53:11 +01:00
|
|
|
import {
|
|
|
|
|
checkAttachmentSize,
|
|
|
|
|
getDefaultSender,
|
|
|
|
|
getEnabledMailAddressesWithUser,
|
|
|
|
|
getSenderNameForUser,
|
|
|
|
|
getTemplateLanguages,
|
2022-01-07 15:58:30 +01:00
|
|
|
RecipientField,
|
2021-02-03 17:13:38 +01:00
|
|
|
} from "../model/MailUtils"
|
2022-04-28 17:32:27 +02:00
|
|
|
import type {File as TutanotaFile, Mail} from "../../api/entities/tutanota/TypeRefs.js"
|
|
|
|
|
import {ContactTypeRef, ConversationEntryTypeRef, FileTypeRef, MailTypeRef} from "../../api/entities/tutanota/TypeRefs.js"
|
2021-02-03 17:13:38 +01:00
|
|
|
import {FileNotFoundError} from "../../api/common/error/FileNotFoundError"
|
|
|
|
|
import type {LoginController} from "../../api/main/LoginController"
|
|
|
|
|
import {logins} from "../../api/main/LoginController"
|
2021-04-29 15:09:21 +02:00
|
|
|
import type {MailboxDetail, MailModel} from "../model/MailModel"
|
2021-02-03 17:13:38 +01:00
|
|
|
import {RecipientNotResolvedError} from "../../api/common/error/RecipientNotResolvedError"
|
2022-01-07 15:58:30 +01:00
|
|
|
import stream from "mithril/stream"
|
|
|
|
|
import Stream from "mithril/stream"
|
2022-03-17 17:15:36 +01:00
|
|
|
import type {EntityUpdateData} from "../../api/main/EventController"
|
2021-02-03 17:13:38 +01:00
|
|
|
import {EventController, isUpdateForTypeRef} from "../../api/main/EventController"
|
|
|
|
|
import {isMailAddress} from "../../misc/FormatValidator"
|
|
|
|
|
import type {ContactModel} from "../../contacts/model/ContactModel"
|
2021-05-27 15:14:41 +02:00
|
|
|
import type {Language, TranslationKey, TranslationText} from "../../misc/LanguageViewModel"
|
2022-03-17 16:18:17 +01:00
|
|
|
import {getAvailableLanguageCode, getSubstitutedLanguageCode, lang, languages} from "../../misc/LanguageViewModel"
|
2021-02-03 17:13:38 +01:00
|
|
|
import type {IUserController} from "../../api/main/UserController"
|
|
|
|
|
import {RecipientsNotFoundError} from "../../api/common/error/RecipientsNotFoundError"
|
|
|
|
|
import {checkApprovalStatus} from "../../misc/LoginUtils"
|
|
|
|
|
import {EntityClient} from "../../api/common/EntityClient"
|
|
|
|
|
import {locator} from "../../api/main/MainLocator"
|
|
|
|
|
import {getContactDisplayName} from "../../contacts/model/ContactUtils"
|
2021-12-23 14:03:23 +01:00
|
|
|
import {getListId, isSameId, stringToCustomId} from "../../api/common/utils/EntityUtils"
|
2022-04-14 17:31:36 +02:00
|
|
|
import {CustomerPropertiesTypeRef} from "../../api/entities/sys/TypeRefs.js"
|
2021-07-02 14:34:05 +02:00
|
|
|
import type {InlineImages} from "../view/MailViewer"
|
|
|
|
|
import {cloneInlineImages, revokeInlineImages} from "../view/MailGuiUtils"
|
2021-07-09 14:38:10 +02:00
|
|
|
import {MailBodyTooLargeError} from "../../api/common/error/MailBodyTooLargeError"
|
2021-08-06 17:44:09 +02:00
|
|
|
import type {MailFacade} from "../../api/worker/facades/MailFacade"
|
2021-11-04 14:05:23 +01:00
|
|
|
import {assertMainOrNode} from "../../api/common/Env"
|
2021-12-23 14:03:23 +01:00
|
|
|
import {DataFile} from "../../api/common/DataFile";
|
2021-12-28 13:53:11 +01:00
|
|
|
import {FileReference} from "../../api/common/utils/FileUtils"
|
2022-03-17 17:15:36 +01:00
|
|
|
import {PartialRecipient, Recipient, RecipientList, Recipients, RecipientType} from "../../api/common/recipients/Recipient"
|
2022-04-28 17:32:27 +02:00
|
|
|
import {RecipientsModel, ResolvableRecipient, ResolveMode} from "../../api/main/RecipientsModel"
|
|
|
|
|
import {createApprovalMail} from "../../api/entities/monitor/TypeRefs"
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
assertMainOrNode()
|
2021-12-23 14:03:23 +01:00
|
|
|
export const TOO_MANY_VISIBLE_RECIPIENTS = 10
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
export type Attachment = TutanotaFile | DataFile | FileReference
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
export type InitAsResponseArgs = {
|
2021-12-28 13:53:11 +01:00
|
|
|
previousMail: Mail
|
|
|
|
|
conversationType: ConversationType
|
|
|
|
|
senderMailAddress: string
|
2022-03-17 17:15:36 +01:00
|
|
|
recipients: Recipients
|
2021-12-28 13:53:11 +01:00
|
|
|
attachments: TutanotaFile[]
|
|
|
|
|
subject: string
|
|
|
|
|
bodyText: string
|
2022-03-17 17:15:36 +01:00
|
|
|
replyTos: RecipientList
|
2020-09-18 14:41:31 +02:00
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
type InitArgs = {
|
|
|
|
|
conversationType: ConversationType
|
|
|
|
|
subject: string
|
|
|
|
|
bodyText: string
|
|
|
|
|
recipients: Recipients
|
2022-04-28 17:32:27 +02:00
|
|
|
confidential: boolean | null
|
|
|
|
|
draft?: Mail | null
|
2022-03-17 16:18:17 +01:00
|
|
|
senderMailAddress?: string
|
|
|
|
|
attachments?: ReadonlyArray<Attachment>
|
2022-03-17 17:15:36 +01:00
|
|
|
replyTos?: RecipientList
|
2022-04-28 17:32:27 +02:00
|
|
|
previousMail?: Mail | null
|
|
|
|
|
previousMessageId?: string | null
|
2022-03-17 16:18:17 +01:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
/**
|
|
|
|
|
* Model which allows sending mails interactively - including resolving of recipients and handling of drafts.
|
|
|
|
|
*/
|
|
|
|
|
export class SendMailModel {
|
2022-03-17 16:18:17 +01:00
|
|
|
|
|
|
|
|
onMailChanged: Stream<boolean> = stream(false)
|
2022-03-17 17:15:36 +01:00
|
|
|
onRecipientDeleted: Stream<{field: RecipientField, recipient: Recipient} | null> = stream(null)
|
2022-03-17 16:18:17 +01:00
|
|
|
onBeforeSend: () => void = noOp
|
|
|
|
|
loadedInlineImages: InlineImages = new Map()
|
|
|
|
|
|
2022-01-13 11:57:55 +01:00
|
|
|
// Isn't private because used by MinimizedEditorOverlay, refactor?
|
2022-03-17 16:18:17 +01:00
|
|
|
draft: Mail | null = null
|
|
|
|
|
private conversationType: ConversationType = ConversationType.NEW
|
|
|
|
|
private subject: string = ""
|
|
|
|
|
private body: string = ""
|
2022-03-17 17:15:36 +01:00
|
|
|
private recipients: Map<RecipientField, Array<ResolvableRecipient>> = new Map()
|
2022-03-17 16:18:17 +01:00
|
|
|
private senderAddress: string
|
|
|
|
|
private confidential: boolean
|
|
|
|
|
|
|
|
|
|
// contains either Files from Tutanota or DataFiles of locally loaded files. these map 1:1 to the _attachmentButtons
|
|
|
|
|
private attachments: Array<Attachment> = []
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
private replyTos: Array<ResolvableRecipient> = []
|
2022-03-17 16:18:17 +01:00
|
|
|
|
|
|
|
|
// only needs to be the correct value if this is a new email. if we are editing a draft, conversationType is not used
|
|
|
|
|
private previousMessageId: Id | null = null
|
|
|
|
|
|
|
|
|
|
private previousMail: Mail | null = null
|
|
|
|
|
private selectedNotificationLanguage: string
|
|
|
|
|
private availableNotificationTemplateLanguages: Array<Language> = []
|
|
|
|
|
private mailChanged: boolean = false
|
|
|
|
|
private passwords: Map<string, string> = new Map()
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-02-04 17:20:32 +01:00
|
|
|
// The promise for the draft currently being saved
|
|
|
|
|
private currentSavePromise: Promise<void> | null = null
|
|
|
|
|
|
|
|
|
|
// If saveDraft is called while the previous call is still running, then flag to call again afterwards
|
|
|
|
|
private doSaveAgain: boolean = false
|
|
|
|
|
|
2021-12-28 13:53:11 +01:00
|
|
|
/**
|
|
|
|
|
* creates a new empty draft message. calling an init method will fill in all the blank data
|
|
|
|
|
*/
|
|
|
|
|
constructor(
|
2022-03-17 16:18:17 +01:00
|
|
|
public readonly mailFacade: MailFacade,
|
|
|
|
|
public readonly entity: EntityClient,
|
|
|
|
|
public readonly logins: LoginController,
|
|
|
|
|
public readonly mailModel: MailModel,
|
|
|
|
|
public readonly contactModel: ContactModel,
|
|
|
|
|
private readonly eventController: EventController,
|
2022-03-17 17:15:36 +01:00
|
|
|
public readonly mailboxDetails: MailboxDetail,
|
|
|
|
|
private readonly recipientsModel: RecipientsModel,
|
2021-12-28 13:53:11 +01:00
|
|
|
) {
|
|
|
|
|
const userProps = logins.getUserController().props
|
2022-03-17 16:18:17 +01:00
|
|
|
this.senderAddress = this.getDefaultSender()
|
|
|
|
|
this.confidential = !userProps.defaultUnconfidential
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.selectedNotificationLanguage = getAvailableLanguageCode(userProps.notificationMailLanguage || lang.code)
|
|
|
|
|
this.updateAvailableNotificationTemplateLanguages()
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.eventController.addEntityListener(updates => this.entityEventReceived(updates))
|
|
|
|
|
}
|
2022-04-28 12:27:56 +02:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private async entityEventReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
|
|
|
|
|
for (let update of updates) {
|
|
|
|
|
await this.handleEntityEvent(update)
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sort list of all languages alphabetically
|
|
|
|
|
* then we see if the user has custom notification templates
|
|
|
|
|
* in which case we replace the list with just the templates that the user has specified
|
|
|
|
|
*/
|
2022-03-17 16:18:17 +01:00
|
|
|
private async updateAvailableNotificationTemplateLanguages(): Promise<void> {
|
|
|
|
|
this.availableNotificationTemplateLanguages = languages.slice().sort((a, b) => lang.get(a.textId).localeCompare(lang.get(b.textId)))
|
|
|
|
|
const filteredLanguages = await getTemplateLanguages(this.availableNotificationTemplateLanguages, this.entity, this.logins)
|
|
|
|
|
if (filteredLanguages.length > 0) {
|
|
|
|
|
const languageCodes = filteredLanguages.map(l => l.code)
|
|
|
|
|
this.selectedNotificationLanguage =
|
|
|
|
|
getSubstitutedLanguageCode(this.logins.getUserController().props.notificationMailLanguage || lang.code, languageCodes) || languageCodes[0]
|
|
|
|
|
this.availableNotificationTemplateLanguages = filteredLanguages
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user(): IUserController {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.logins.getUserController()
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPreviousMail(): Mail | null {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.previousMail
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConversationType(): ConversationType {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.conversationType
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPassword(mailAddress: string, password: string) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.passwords.set(mailAddress, password)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPassword(mailAddress: string): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.passwords.get(mailAddress) || ""
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSubject(): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.subject
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSubject(subject: string) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.mailChanged = subject !== this.subject
|
|
|
|
|
this.subject = subject
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getBody(): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.body
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setBody(body: string) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.body = body
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSender(senderAddress: string) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.senderAddress = senderAddress
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSender(): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.senderAddress
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the strength indicator for the recipients password
|
|
|
|
|
* @returns value between 0 and 100
|
|
|
|
|
*/
|
2022-03-17 17:15:36 +01:00
|
|
|
getPasswordStrength(recipient: PartialRecipient): number {
|
|
|
|
|
return getPasswordStrengthForUser(this.getPassword(recipient.address), recipient, this.mailboxDetails, this.logins)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getEnabledMailAddresses(): Array<string> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return getEnabledMailAddressesWithUser(this.mailboxDetails, this.user().userGroupInfo)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasMailChanged(): boolean {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.mailChanged
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setMailChanged(hasChanged: boolean) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.mailChanged = hasChanged
|
2021-12-28 13:53:11 +01:00
|
|
|
this.onMailChanged(hasChanged) // if this method is called wherever state gets changed, onMailChanged should function properly
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param recipients
|
|
|
|
|
* @param subject
|
|
|
|
|
* @param bodyText
|
|
|
|
|
* @param attachments
|
|
|
|
|
* @param confidential
|
|
|
|
|
* @param senderMailAddress
|
|
|
|
|
* @returns {Promise<SendMailModel>}
|
|
|
|
|
*/
|
|
|
|
|
initWithTemplate(
|
2022-01-07 15:58:30 +01:00
|
|
|
recipients: Recipients,
|
|
|
|
|
subject: string,
|
|
|
|
|
bodyText: string,
|
|
|
|
|
attachments?: ReadonlyArray<Attachment>,
|
|
|
|
|
confidential?: boolean,
|
|
|
|
|
senderMailAddress?: string,
|
2021-12-28 13:53:11 +01:00
|
|
|
): Promise<SendMailModel> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.init({
|
2021-12-28 13:53:11 +01:00
|
|
|
conversationType: ConversationType.NEW,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
attachments,
|
2022-04-28 17:32:27 +02:00
|
|
|
confidential: confidential ?? null,
|
2021-12-28 13:53:11 +01:00
|
|
|
senderMailAddress,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
async initAsResponse(args: InitAsResponseArgs, inlineImages: InlineImages): Promise<SendMailModel> {
|
2021-12-28 13:53:11 +01:00
|
|
|
const {
|
|
|
|
|
previousMail,
|
|
|
|
|
conversationType,
|
|
|
|
|
senderMailAddress,
|
2022-03-17 17:15:36 +01:00
|
|
|
recipients,
|
2021-12-28 13:53:11 +01:00
|
|
|
attachments,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
replyTos
|
|
|
|
|
} = args
|
|
|
|
|
let previousMessageId: string | null = null
|
2022-03-17 16:18:17 +01:00
|
|
|
await this.entity
|
2022-01-07 15:58:30 +01:00
|
|
|
.load(ConversationEntryTypeRef, previousMail.conversationEntry)
|
|
|
|
|
.then(ce => {
|
|
|
|
|
previousMessageId = ce.messageId
|
|
|
|
|
})
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(NotFoundError, e => {
|
|
|
|
|
console.log("could not load conversation entry", e)
|
|
|
|
|
}),
|
|
|
|
|
)
|
2021-12-28 13:53:11 +01:00
|
|
|
// if we reuse the same image references, changing the displayed mail in mail view will cause the minimized draft to lose
|
|
|
|
|
// that reference, because it will be revoked
|
2022-04-07 17:00:54 +02:00
|
|
|
this.loadedInlineImages = cloneInlineImages(inlineImages)
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.init({
|
2021-12-28 13:53:11 +01:00
|
|
|
conversationType,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
senderMailAddress,
|
|
|
|
|
confidential: previousMail.confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-07 17:00:54 +02:00
|
|
|
async initWithDraft(draft: Mail, attachments: TutanotaFile[], bodyText: string, inlineImages: InlineImages): Promise<SendMailModel> {
|
2021-12-28 13:53:11 +01:00
|
|
|
let previousMessageId: string | null = null
|
|
|
|
|
let previousMail: Mail | null = null
|
2022-02-03 14:55:40 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
const conversationEntry = await this.entity.load(ConversationEntryTypeRef, draft.conversationEntry)
|
2022-02-03 14:55:40 +01:00
|
|
|
const conversationType = downcast<ConversationType>(conversationEntry.conversationType)
|
|
|
|
|
|
|
|
|
|
if (conversationEntry.previous) {
|
|
|
|
|
try {
|
2022-03-17 16:18:17 +01:00
|
|
|
const previousEntry = await this.entity.load(ConversationEntryTypeRef, conversationEntry.previous)
|
2022-02-03 14:55:40 +01:00
|
|
|
previousMessageId = previousEntry.messageId
|
|
|
|
|
if (previousEntry.mail) {
|
2022-03-17 16:18:17 +01:00
|
|
|
previousMail = await this.entity.load(MailTypeRef, previousEntry.mail)
|
2022-02-03 14:55:40 +01:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof NotFoundError) {
|
|
|
|
|
// ignore
|
|
|
|
|
} else {
|
|
|
|
|
throw e
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
2022-02-03 14:55:40 +01:00
|
|
|
}
|
|
|
|
|
|
2021-12-28 13:53:11 +01:00
|
|
|
// if we reuse the same image references, changing the displayed mail in mail view will cause the minimized draft to lose
|
|
|
|
|
// that reference, because it will be revoked
|
2022-04-07 17:00:54 +02:00
|
|
|
this.loadedInlineImages = cloneInlineImages(inlineImages)
|
2021-12-28 13:53:11 +01:00
|
|
|
const {confidential, sender, toRecipients, ccRecipients, bccRecipients, subject, replyTos} = draft
|
|
|
|
|
const recipients: Recipients = {
|
2022-03-17 17:15:36 +01:00
|
|
|
to: toRecipients,
|
|
|
|
|
cc: ccRecipients,
|
|
|
|
|
bcc: bccRecipients,
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.init({
|
2021-12-28 13:53:11 +01:00
|
|
|
conversationType: conversationType,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
draft,
|
|
|
|
|
senderMailAddress: sender.address,
|
|
|
|
|
confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private async init(
|
|
|
|
|
{
|
|
|
|
|
conversationType,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
draft,
|
|
|
|
|
recipients,
|
|
|
|
|
senderMailAddress,
|
|
|
|
|
confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId,
|
|
|
|
|
}: InitArgs
|
|
|
|
|
): Promise<SendMailModel> {
|
|
|
|
|
this.conversationType = conversationType
|
|
|
|
|
this.subject = subject
|
|
|
|
|
this.body = bodyText
|
2022-02-04 17:20:32 +01:00
|
|
|
this.draft = draft || null
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
let to: RecipientList
|
|
|
|
|
let cc: RecipientList
|
|
|
|
|
let bcc: RecipientList
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
if (recipients instanceof Array) {
|
|
|
|
|
to = recipients
|
|
|
|
|
cc = []
|
|
|
|
|
bcc = []
|
|
|
|
|
} else {
|
|
|
|
|
to = recipients.to ?? []
|
|
|
|
|
cc = recipients.cc ?? []
|
|
|
|
|
bcc = recipients.bcc ?? []
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-15 10:27:49 +02:00
|
|
|
const recipientsFilter = (recipientList: Array<PartialRecipient>) => deduplicate(
|
|
|
|
|
recipientList.filter(r => isMailAddress(r.address, false)),
|
|
|
|
|
(a, b) => a.address === b.address,
|
|
|
|
|
)
|
2022-03-17 17:15:36 +01:00
|
|
|
|
2022-06-15 10:27:49 +02:00
|
|
|
recipientsFilter(to).map(r => this.addRecipient(RecipientField.TO, r))
|
|
|
|
|
recipientsFilter(cc).map(r => this.addRecipient(RecipientField.CC, r))
|
|
|
|
|
recipientsFilter(bcc).map(r => this.addRecipient(RecipientField.BCC, r))
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.senderAddress = senderMailAddress || this.getDefaultSender()
|
2022-06-15 10:27:49 +02:00
|
|
|
this.confidential = confidential ?? !this.user().props.defaultUnconfidential
|
2022-03-17 16:18:17 +01:00
|
|
|
this.attachments = []
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
if (attachments) {
|
|
|
|
|
this.attachFiles(attachments)
|
2022-03-17 16:18:17 +01:00
|
|
|
this.mailChanged = false
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-15 10:27:49 +02:00
|
|
|
this.replyTos = recipientsFilter(replyTos ?? []).map(recipient => this.recipientsModel.resolve(recipient, ResolveMode.Eager))
|
2022-03-17 16:18:17 +01:00
|
|
|
this.previousMail = previousMail || null
|
|
|
|
|
this.previousMessageId = previousMessageId || null
|
|
|
|
|
this.mailChanged = false
|
|
|
|
|
return this
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private getDefaultSender(): string {
|
|
|
|
|
return getDefaultSender(this.logins, this.mailboxDetails)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
getRecipientList(type: RecipientField): Array<ResolvableRecipient> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return getFromMap(this.recipients, type, () => [])
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
toRecipients(): Array<ResolvableRecipient> {
|
2022-01-07 15:58:30 +01:00
|
|
|
return this.getRecipientList(RecipientField.TO)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
toRecipientsResolved(): Promise<Array<Recipient>> {
|
|
|
|
|
return Promise.all(this.toRecipients().map(recipient => recipient.resolved()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ccRecipients(): Array<ResolvableRecipient> {
|
2022-01-07 15:58:30 +01:00
|
|
|
return this.getRecipientList(RecipientField.CC)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
ccRecipientsResolved(): Promise<Array<Recipient>> {
|
|
|
|
|
return Promise.all(this.ccRecipients().map(recipient => recipient.resolved()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bccRecipients(): Array<ResolvableRecipient> {
|
2022-01-07 15:58:30 +01:00
|
|
|
return this.getRecipientList(RecipientField.BCC)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
bccRecipientsResolved(): Promise<Array<Recipient>> {
|
|
|
|
|
return Promise.all(this.bccRecipients().map(recipient => recipient.resolved()))
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
replyTosResolved(): Promise<Array<Recipient>> {
|
|
|
|
|
return Promise.all(this.replyTos.map(r => r.resolved()))
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
/**
|
|
|
|
|
* Add a new recipient, this method resolves when the recipient resolves
|
|
|
|
|
*/
|
|
|
|
|
async addRecipient(
|
|
|
|
|
fieldType: RecipientField,
|
|
|
|
|
{
|
|
|
|
|
address,
|
|
|
|
|
name,
|
|
|
|
|
type,
|
|
|
|
|
contact
|
|
|
|
|
}: PartialRecipient,
|
|
|
|
|
resolveMode: ResolveMode = ResolveMode.Eager
|
|
|
|
|
): Promise<void> {
|
2022-03-17 17:15:36 +01:00
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
let recipient = this.getRecipientList(fieldType).find(recipient => recipient.address === address)
|
2022-03-17 17:15:36 +01:00
|
|
|
// Only add a recipient if it doesn't exist
|
2022-04-28 17:32:27 +02:00
|
|
|
if (!recipient) {
|
|
|
|
|
recipient = this.recipientsModel.resolve({
|
|
|
|
|
address,
|
|
|
|
|
name,
|
|
|
|
|
type,
|
|
|
|
|
contact,
|
|
|
|
|
},
|
|
|
|
|
resolveMode
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
this.getRecipientList(fieldType).push(recipient)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
recipient.resolved().then(({address, contact}) => {
|
2022-03-17 17:15:36 +01:00
|
|
|
if (!this.passwords.has(address) && contact != null) {
|
|
|
|
|
this.setPassword(address, contact.presharedPassword ?? "")
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
})
|
2022-03-17 17:15:36 +01:00
|
|
|
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
2022-04-28 17:32:27 +02:00
|
|
|
|
|
|
|
|
await recipient.resolved()
|
2022-03-17 17:15:36 +01:00
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
getRecipient(type: RecipientField, address: string): ResolvableRecipient | null {
|
|
|
|
|
return this.getRecipientList(type).find(recipient => recipient.address === address) ?? null
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
removeRecipientByAddress(address: string, type: RecipientField, notify: boolean = true) {
|
|
|
|
|
const recipient = this.getRecipientList(type).find(recipient => recipient.address === address)
|
|
|
|
|
if (recipient) {
|
|
|
|
|
this.removeRecipient(recipient, type, notify)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
removeRecipient(recipient: Recipient, type: RecipientField, notify: boolean = true): boolean {
|
2022-04-28 17:32:27 +02:00
|
|
|
const recipients = this.recipients.get(type) ?? []
|
|
|
|
|
const didRemove = findAndRemove(recipients, r => r.address === recipient.address)
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(didRemove)
|
|
|
|
|
|
|
|
|
|
if (didRemove && notify) {
|
|
|
|
|
this.onRecipientDeleted({
|
|
|
|
|
field: type,
|
|
|
|
|
recipient,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return didRemove
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose() {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.eventController.removeEntityListener(this.entityEventReceived)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
revokeInlineImages(this.loadedInlineImages)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @throws UserError in the case that any files were too big to attach. Small enough files will still have been attached
|
|
|
|
|
*/
|
|
|
|
|
getAttachments(): Array<Attachment> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.attachments
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @throws UserError in case files are too big to add */
|
|
|
|
|
attachFiles(files: ReadonlyArray<Attachment>): void {
|
2022-03-17 16:18:17 +01:00
|
|
|
let sizeLeft = MAX_ATTACHMENT_SIZE - this.attachments.reduce((total, file) => total + Number(file.size), 0)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
const sizeCheckResult = checkAttachmentSize(files, sizeLeft)
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.attachments.push(...sizeCheckResult.attachableFiles)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
|
|
|
|
|
if (sizeCheckResult.tooBigFiles.length > 0) {
|
|
|
|
|
throw new UserError(() => lang.get("tooBigAttachment_msg") + "\n" + sizeCheckResult.tooBigFiles.join("\n"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeAttachment(file: Attachment): void {
|
2022-03-17 16:18:17 +01:00
|
|
|
if (remove(this.attachments, file)) {
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSenderName(): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return getSenderNameForUser(this.mailboxDetails, this.user())
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDraft(): Readonly<Mail> | null {
|
2022-02-04 17:20:32 +01:00
|
|
|
return this.draft
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
private async updateDraft(body: string, attachments: ReadonlyArray<Attachment> | null, draft: Mail): Promise<Mail> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.mailFacade
|
2022-01-07 15:58:30 +01:00
|
|
|
.updateDraft(
|
2022-02-03 14:55:40 +01:00
|
|
|
{
|
|
|
|
|
subject: this.getSubject(),
|
|
|
|
|
body: body,
|
2022-03-17 16:18:17 +01:00
|
|
|
senderMailAddress: this.senderAddress,
|
2022-02-03 14:55:40 +01:00
|
|
|
senderName: this.getSenderName(),
|
2022-03-17 17:15:36 +01:00
|
|
|
toRecipients: await this.toRecipientsResolved(),
|
|
|
|
|
ccRecipients: await this.ccRecipientsResolved(),
|
|
|
|
|
bccRecipients: await this.bccRecipientsResolved(),
|
2022-02-03 14:55:40 +01:00
|
|
|
attachments: attachments,
|
|
|
|
|
confidential: this.isConfidential(),
|
2022-03-16 10:14:53 +01:00
|
|
|
draft: draft,
|
2022-02-03 14:55:40 +01:00
|
|
|
},
|
2022-01-07 15:58:30 +01:00
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(LockedError, e => {
|
|
|
|
|
console.log("updateDraft: operation is still active", e)
|
|
|
|
|
throw new UserError("operationStillActive_msg")
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(NotFoundError, e => {
|
|
|
|
|
console.log("draft has been deleted, creating new one")
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.createDraft(body, attachments, downcast(draft.method))
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
private async createDraft(body: string, attachments: ReadonlyArray<Attachment> | null, mailMethod: MailMethod): Promise<Mail> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.mailFacade.createDraft(
|
2022-02-03 14:55:40 +01:00
|
|
|
{
|
|
|
|
|
subject: this.getSubject(),
|
|
|
|
|
bodyText: body,
|
2022-03-17 16:18:17 +01:00
|
|
|
senderMailAddress: this.senderAddress,
|
2022-02-03 14:55:40 +01:00
|
|
|
senderName: this.getSenderName(),
|
2022-03-17 17:15:36 +01:00
|
|
|
toRecipients: await this.toRecipientsResolved(),
|
|
|
|
|
ccRecipients: await this.ccRecipientsResolved(),
|
|
|
|
|
bccRecipients: await this.bccRecipientsResolved(),
|
2022-03-17 16:18:17 +01:00
|
|
|
conversationType: this.conversationType,
|
|
|
|
|
previousMessageId: this.previousMessageId,
|
2022-02-03 14:55:40 +01:00
|
|
|
attachments: attachments,
|
|
|
|
|
confidential: this.isConfidential(),
|
2022-03-17 17:15:36 +01:00
|
|
|
replyTos: await this.replyTosResolved(),
|
2022-02-03 14:55:40 +01:00
|
|
|
method: mailMethod
|
|
|
|
|
},
|
2021-12-28 13:53:11 +01:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isConfidential(): boolean {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.confidential || !this.containsExternalRecipients()
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isConfidentialExternal(): boolean {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.confidential && this.containsExternalRecipients()
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setConfidential(confidential: boolean): void {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.confidential = confidential
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
containsExternalRecipients(): boolean {
|
2022-03-17 17:15:36 +01:00
|
|
|
return this.allRecipients().some(r => r.type === RecipientType.EXTERNAL)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
getExternalRecipients(): Array<Recipient> {
|
|
|
|
|
return this.allRecipients().filter(r => r.type === RecipientType.EXTERNAL)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @reject {RecipientsNotFoundError}
|
|
|
|
|
* @reject {TooManyRequestsError}
|
|
|
|
|
* @reject {AccessBlockedError}
|
|
|
|
|
* @reject {FileNotFoundError}
|
|
|
|
|
* @reject {PreconditionFailedError}
|
|
|
|
|
* @reject {LockedError}
|
|
|
|
|
* @reject {UserError}
|
|
|
|
|
* @param mailMethod
|
2022-02-04 17:20:32 +01:00
|
|
|
* @param getConfirmation: A callback to get user confirmation
|
|
|
|
|
* @param waitHandler: A callback to allow UI blocking while the mail is being sent. it seems like wrapping the send call in showProgressDialog causes the confirmation dialogs not to be shown. We should fix this, but this works for now
|
2021-12-28 13:53:11 +01:00
|
|
|
* @param tooManyRequestsError
|
|
|
|
|
* @return true if the send was completed, false if it was aborted (by getConfirmation returning false
|
|
|
|
|
*/
|
|
|
|
|
async send(
|
2022-01-07 15:58:30 +01:00
|
|
|
mailMethod: MailMethod,
|
|
|
|
|
getConfirmation: (arg0: TranslationText) => Promise<boolean> = _ => Promise.resolve(true),
|
|
|
|
|
waitHandler: (arg0: TranslationText, arg1: Promise<any>) => Promise<any> = (_, p) => p,
|
|
|
|
|
tooManyRequestsError: TranslationKey = "tooManyMails_msg",
|
2021-12-28 13:53:11 +01:00
|
|
|
): Promise<boolean> {
|
|
|
|
|
this.onBeforeSend()
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
if (this.allRecipients().length === 1 && this.allRecipients()[0].address.toLowerCase().trim() === "approval@tutao.de") {
|
2022-03-17 16:18:17 +01:00
|
|
|
await this.sendApprovalMail(this.getBody())
|
2021-12-28 13:53:11 +01:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.toRecipients().length === 0 && this.ccRecipients().length === 0 && this.bccRecipients().length === 0) {
|
|
|
|
|
throw new UserError("noRecipients_msg")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const numVisibleRecipients = this.toRecipients().length + this.ccRecipients().length
|
|
|
|
|
|
|
|
|
|
// Many recipients is a warning
|
|
|
|
|
if (numVisibleRecipients >= TOO_MANY_VISIBLE_RECIPIENTS && !(await getConfirmation("manyRecipients_msg"))) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Empty subject is a warning
|
|
|
|
|
if (this.getSubject().length === 0 && !(await getConfirmation("noSubject_msg"))) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The next check depends on contacts being available
|
2022-03-17 17:15:36 +01:00
|
|
|
// So we need to wait for our recipients here
|
|
|
|
|
const recipients = await this.waitForResolvedRecipients()
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
// No password in external confidential mail is an error
|
2022-03-17 17:15:36 +01:00
|
|
|
if (this.isConfidentialExternal() && this.getExternalRecipients().some(r => !this.getPassword(r.address))) {
|
2021-12-28 13:53:11 +01:00
|
|
|
throw new UserError("noPreSharedPassword_msg")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Weak password is a warning
|
|
|
|
|
if (this.isConfidentialExternal() && this.hasInsecurePasswords() && !(await getConfirmation("presharedPasswordNotStrongEnough_msg"))) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const doSend = async () => {
|
2022-02-04 17:20:32 +01:00
|
|
|
await this.saveDraft(true, mailMethod)
|
2022-03-17 17:15:36 +01:00
|
|
|
await this.updateContacts(recipients)
|
|
|
|
|
await this.mailFacade.sendDraft(neverNull(this.draft), recipients, this.selectedNotificationLanguage)
|
2022-03-17 16:18:17 +01:00
|
|
|
await this.updatePreviousMail()
|
|
|
|
|
await this.updateExternalLanguage()
|
2021-12-28 13:53:11 +01:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return waitHandler(this.isConfidential() ? "sending_msg" : "sendingUnencrypted_msg", doSend())
|
2022-01-07 15:58:30 +01:00
|
|
|
.catch(
|
|
|
|
|
ofClass(LockedError, () => {
|
2022-03-17 16:18:17 +01:00
|
|
|
throw new UserError("operationStillActive_msg")
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
) // catch all of the badness
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(RecipientNotResolvedError, () => {
|
2022-03-17 16:18:17 +01:00
|
|
|
throw new UserError("tooManyAttempts_msg")
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(RecipientsNotFoundError, e => {
|
2022-03-17 16:18:17 +01:00
|
|
|
if (mailMethod === MailMethod.ICAL_CANCEL) {
|
|
|
|
|
// in case of calendar event cancellation we will remove invalid recipients and then delete the event without sending updates
|
|
|
|
|
throw e
|
|
|
|
|
} else {
|
|
|
|
|
let invalidRecipients = e.message
|
|
|
|
|
throw new UserError(
|
|
|
|
|
() => lang.get("tutanotaAddressDoesNotExist_msg") + " " + lang.get("invalidRecipients_msg") + "\n" + invalidRecipients,
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(TooManyRequestsError, () => {
|
2022-03-17 16:18:17 +01:00
|
|
|
throw new UserError(tooManyRequestsError)
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(AccessBlockedError, e => {
|
2022-03-17 16:18:17 +01:00
|
|
|
// special case: the approval status is set to SpamSender, but the update has not been received yet, so use SpamSender as default
|
|
|
|
|
return checkApprovalStatus(this.logins, true, ApprovalStatus.SPAM_SENDER).then(() => {
|
|
|
|
|
console.log("could not send mail (blocked access)", e)
|
|
|
|
|
return false
|
|
|
|
|
})
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(FileNotFoundError, () => {
|
2022-03-17 16:18:17 +01:00
|
|
|
throw new UserError("couldNotAttachFile_msg")
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.catch(
|
|
|
|
|
ofClass(PreconditionFailedError, () => {
|
2022-03-17 16:18:17 +01:00
|
|
|
throw new UserError("operationStillActive_msg")
|
2022-01-07 15:58:30 +01:00
|
|
|
}),
|
|
|
|
|
)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether any of the external recipients have an insecure password.
|
|
|
|
|
* We don't consider empty passwords, because an empty password will disallow and encrypted email from sending, whereas an insecure password
|
|
|
|
|
* can still be used
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
*/
|
|
|
|
|
hasInsecurePasswords(): boolean {
|
|
|
|
|
const minimalPasswordStrength = this.allRecipients()
|
2022-03-17 17:15:36 +01:00
|
|
|
.filter(r => this.getPassword(r.address) !== "")
|
2022-01-07 15:58:30 +01:00
|
|
|
.reduce((min, recipient) => Math.min(min, this.getPasswordStrength(recipient)), PASSWORD_MIN_SECURE_VALUE)
|
2021-12-28 13:53:11 +01:00
|
|
|
return !isSecurePassword(minimalPasswordStrength)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-04 17:20:32 +01:00
|
|
|
saveDraft(
|
|
|
|
|
saveAttachments: boolean,
|
|
|
|
|
mailMethod: MailMethod
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
|
|
|
|
if (this.currentSavePromise == null) {
|
|
|
|
|
this.currentSavePromise = Promise.resolve().then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await this.doSaveDraft(saveAttachments, mailMethod)
|
|
|
|
|
} finally {
|
|
|
|
|
// If there is an error, we still need to reset currentSavePromise
|
|
|
|
|
this.currentSavePromise = null
|
|
|
|
|
}
|
2022-03-17 16:18:17 +01:00
|
|
|
if (this.mailChanged && this.doSaveAgain) {
|
2022-02-04 17:20:32 +01:00
|
|
|
this.doSaveAgain = false
|
|
|
|
|
await this.saveDraft(saveAttachments, mailMethod)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
this.doSaveAgain = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.currentSavePromise
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-28 13:53:11 +01:00
|
|
|
/**
|
|
|
|
|
* Saves the draft.
|
|
|
|
|
* @param saveAttachments True if also the attachments shall be saved, false otherwise.
|
2022-02-04 17:20:32 +01:00
|
|
|
* @param mailMethod
|
2021-12-28 13:53:11 +01:00
|
|
|
* @returns {Promise} When finished.
|
|
|
|
|
* @throws FileNotFoundError when one of the attachments could not be opened
|
|
|
|
|
* @throws PreconditionFailedError when the draft is locked
|
|
|
|
|
*/
|
2022-02-04 17:20:32 +01:00
|
|
|
private async doSaveDraft(
|
2022-01-07 15:58:30 +01:00
|
|
|
saveAttachments: boolean,
|
|
|
|
|
mailMethod: MailMethod,
|
2021-12-28 13:53:11 +01:00
|
|
|
): Promise<void> {
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2022-02-04 17:20:32 +01:00
|
|
|
// Allow any changes that might occur while the mail is being saved to be accounted for
|
|
|
|
|
// if saved is called before this has completed
|
2022-03-17 16:18:17 +01:00
|
|
|
this.mailChanged = false
|
2022-02-04 17:20:32 +01:00
|
|
|
|
|
|
|
|
try {
|
2022-03-17 16:18:17 +01:00
|
|
|
const attachments = saveAttachments ? this.attachments : null
|
2022-02-04 17:20:32 +01:00
|
|
|
|
|
|
|
|
// We also want to create new drafts for drafts edited from trash or spam folder
|
|
|
|
|
this.draft = this.draft == null || await this.isMailInTrashOrSpam(this.draft)
|
2022-03-17 16:18:17 +01:00
|
|
|
? await this.createDraft(this.getBody(), attachments, mailMethod)
|
|
|
|
|
: await this.updateDraft(this.getBody(), attachments, this.draft)
|
2022-02-04 17:20:32 +01:00
|
|
|
|
|
|
|
|
const newAttachments = await promiseMap(
|
|
|
|
|
this.draft.attachments,
|
2022-03-17 16:18:17 +01:00
|
|
|
fileId => this.entity.load<TutanotaFile>(FileTypeRef, fileId),
|
2022-02-04 17:20:32 +01:00
|
|
|
{
|
|
|
|
|
concurrency: 5,
|
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
)
|
2022-02-04 17:20:32 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.attachments = [] // attachFiles will push to existing files but we want to overwrite them
|
2022-02-04 17:20:32 +01:00
|
|
|
this.attachFiles(newAttachments)
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof PayloadTooLargeError) {
|
|
|
|
|
throw new UserError("requestTooLarge_msg")
|
|
|
|
|
} else if (e instanceof MailBodyTooLargeError) {
|
|
|
|
|
throw new UserError("mailBodyTooLarge_msg")
|
2022-04-06 16:56:13 +02:00
|
|
|
} else if (e instanceof FileNotFoundError) {
|
|
|
|
|
throw new UserError("couldNotAttachFile_msg")
|
|
|
|
|
} else if (e instanceof PreconditionFailedError) {
|
|
|
|
|
throw new UserError("operationStillActive_msg")
|
2022-02-04 17:20:32 +01:00
|
|
|
} else {
|
|
|
|
|
throw e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async isMailInTrashOrSpam(draft: Mail) {
|
2022-03-17 16:18:17 +01:00
|
|
|
const folders = await this.mailModel.getMailboxFolders(draft)
|
2022-02-04 17:20:32 +01:00
|
|
|
const trashAndMailFolders = folders.filter(f => f.folderType === MailFolderType.TRASH || f.folderType === MailFolderType.SPAM)
|
|
|
|
|
return trashAndMailFolders.some(folder => isSameId(folder.mails, getListId(draft)))
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private sendApprovalMail(body: string): Promise<unknown> {
|
2021-12-28 13:53:11 +01:00
|
|
|
const listId = "---------c--"
|
|
|
|
|
const m = createApprovalMail({
|
2022-03-17 16:18:17 +01:00
|
|
|
_id: [listId, stringToCustomId(this.senderAddress)],
|
2021-12-28 13:53:11 +01:00
|
|
|
_ownerGroup: this.user().user.userGroup.group,
|
|
|
|
|
text: `Subject: ${this.getSubject()}<br>${body}`,
|
|
|
|
|
})
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.entity.setup(listId, m).catch(ofClass(NotAuthorizedError, e => console.log("not authorized for approval message")))
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAvailableNotificationTemplateLanguages(): Array<Language> {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.availableNotificationTemplateLanguages
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSelectedNotificationLanguageCode(): string {
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.selectedNotificationLanguage
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSelectedNotificationLanguageCode(code: string) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.selectedNotificationLanguage = code
|
2021-12-28 13:53:11 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private updateExternalLanguage() {
|
2021-12-28 13:53:11 +01:00
|
|
|
let props = this.user().props
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
if (props.notificationMailLanguage !== this.selectedNotificationLanguage) {
|
|
|
|
|
props.notificationMailLanguage = this.selectedNotificationLanguage
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
this.entity.update(props)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
private updatePreviousMail(): Promise<void> {
|
|
|
|
|
if (this.previousMail) {
|
|
|
|
|
if (this.previousMail.replyType === ReplyType.NONE && this.conversationType === ConversationType.REPLY) {
|
|
|
|
|
this.previousMail.replyType = ReplyType.REPLY
|
|
|
|
|
} else if (this.previousMail.replyType === ReplyType.NONE && this.conversationType === ConversationType.FORWARD) {
|
|
|
|
|
this.previousMail.replyType = ReplyType.FORWARD
|
|
|
|
|
} else if (this.previousMail.replyType === ReplyType.FORWARD && this.conversationType === ConversationType.REPLY) {
|
|
|
|
|
this.previousMail.replyType = ReplyType.REPLY_FORWARD
|
|
|
|
|
} else if (this.previousMail.replyType === ReplyType.REPLY && this.conversationType === ConversationType.FORWARD) {
|
|
|
|
|
this.previousMail.replyType = ReplyType.REPLY_FORWARD
|
2021-12-28 13:53:11 +01:00
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-17 16:18:17 +01:00
|
|
|
return this.entity.update(this.previousMail).catch(ofClass(NotFoundError, noOp))
|
|
|
|
|
|
2021-12-28 13:53:11 +01:00
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
/**
|
|
|
|
|
* If contacts have had their passwords changed, we update them before sending
|
|
|
|
|
*/
|
|
|
|
|
private async updateContacts(resolvedRecipients: Recipient[]): Promise<any> {
|
|
|
|
|
for (const {address, contact, type} of resolvedRecipients) {
|
|
|
|
|
if (contact == null) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
const isExternalAndConfidential = type === RecipientType.EXTERNAL && this.isConfidential()
|
2022-01-07 15:58:30 +01:00
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
if (!contact._id && (!this.user().props.noAutomaticContacts || isExternalAndConfidential)) {
|
|
|
|
|
if (isExternalAndConfidential) {
|
2022-03-17 17:15:36 +01:00
|
|
|
contact.presharedPassword = this.getPassword(address).trim()
|
2022-01-07 15:58:30 +01:00
|
|
|
}
|
2022-04-28 17:32:27 +02:00
|
|
|
|
|
|
|
|
const listId = await this.contactModel.contactListId()
|
|
|
|
|
await this.entity.setup(listId, contact)
|
|
|
|
|
} else if (contact._id && isExternalAndConfidential && contact.presharedPassword !== this.getPassword(address).trim()) {
|
|
|
|
|
contact.presharedPassword = this.getPassword(address).trim()
|
|
|
|
|
await this.entity.update(contact)
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-17 17:15:36 +01:00
|
|
|
allRecipients(): Array<ResolvableRecipient> {
|
2021-12-28 13:53:11 +01:00
|
|
|
return this.toRecipients().concat(this.ccRecipients()).concat(this.bccRecipients())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Makes sure the recipient type and contact are resolved.
|
|
|
|
|
*/
|
2022-03-17 17:15:36 +01:00
|
|
|
waitForResolvedRecipients(): Promise<Recipient[]> {
|
|
|
|
|
return Promise.all(this.allRecipients().map(recipient => recipient.resolved()))
|
|
|
|
|
.catch(ofClass(TooManyRequestsError, () => {
|
|
|
|
|
throw new RecipientNotResolvedError("")
|
|
|
|
|
}))
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
|
2022-04-28 17:32:27 +02:00
|
|
|
handleEntityEvent(update: EntityUpdateData): Promise<void> {
|
2021-12-28 13:53:11 +01:00
|
|
|
const {operation, instanceId, instanceListId} = update
|
|
|
|
|
let contactId: IdTuple = [neverNull(instanceListId), instanceId]
|
|
|
|
|
|
|
|
|
|
if (isUpdateForTypeRef(ContactTypeRef, update)) {
|
|
|
|
|
if (operation === OperationType.UPDATE) {
|
2022-03-17 16:18:17 +01:00
|
|
|
this.entity.load(ContactTypeRef, contactId).then(contact => {
|
2022-01-07 15:58:30 +01:00
|
|
|
for (const fieldType of typedValues(RecipientField)) {
|
2021-12-28 13:53:11 +01:00
|
|
|
const matching = this.getRecipientList(fieldType).filter(recipient => recipient.contact && isSameId(recipient.contact._id, contact._id))
|
|
|
|
|
matching.forEach(recipient => {
|
|
|
|
|
// if the mail address no longer exists on the contact then delete the recipient
|
2022-03-17 17:15:36 +01:00
|
|
|
if (!contact.mailAddresses.find(ma => cleanMatch(ma.address, recipient.address))) {
|
2021-12-28 13:53:11 +01:00
|
|
|
this.removeRecipient(recipient, fieldType, true)
|
|
|
|
|
} else {
|
|
|
|
|
// else just modify the recipient
|
2022-03-17 17:15:36 +01:00
|
|
|
recipient.setName(getContactDisplayName(contact))
|
|
|
|
|
recipient.setContact(contact)
|
2021-12-28 13:53:11 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else if (operation === OperationType.DELETE) {
|
2022-01-07 15:58:30 +01:00
|
|
|
for (const fieldType of typedValues(RecipientField)) {
|
2021-12-28 13:53:11 +01:00
|
|
|
const recipients = this.getRecipientList(fieldType)
|
|
|
|
|
|
2022-01-07 15:58:30 +01:00
|
|
|
const toDelete = recipients.filter(recipient => (recipient.contact && isSameId(recipient.contact._id, contactId)) || false)
|
2021-12-28 13:53:11 +01:00
|
|
|
|
|
|
|
|
for (const r of toDelete) {
|
|
|
|
|
this.removeRecipient(r, fieldType, true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
} else if (isUpdateForTypeRef(CustomerPropertiesTypeRef, update)) {
|
|
|
|
|
this.updateAvailableNotificationTemplateLanguages()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOnBeforeSendFunction(fun: () => unknown) {
|
|
|
|
|
this.onBeforeSend = fun
|
|
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
}
|
2021-12-28 13:53:11 +01:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
export function defaultSendMailModel(mailboxDetails: MailboxDetail): SendMailModel {
|
2022-03-17 17:15:36 +01:00
|
|
|
return new SendMailModel(
|
|
|
|
|
locator.mailFacade,
|
|
|
|
|
locator.entityClient,
|
|
|
|
|
logins,
|
|
|
|
|
locator.mailModel,
|
|
|
|
|
locator.contactModel,
|
|
|
|
|
locator.eventController,
|
|
|
|
|
mailboxDetails,
|
|
|
|
|
locator.recipientsModel
|
|
|
|
|
)
|
2021-05-27 15:14:41 +02:00
|
|
|
}
|