2019-08-22 18:24:32 +02:00
|
|
|
// @flow
|
|
|
|
|
import type {ConversationTypeEnum, MailMethodEnum} from "../api/common/TutanotaConstants"
|
2020-12-29 12:07:33 +01:00
|
|
|
import {ConversationType, MailFolderType, MAX_ATTACHMENT_SIZE, OperationType, ReplyType} from "../api/common/TutanotaConstants"
|
2019-08-22 18:24:32 +02:00
|
|
|
import type {RecipientInfo} from "../api/common/RecipientInfo"
|
|
|
|
|
import {isExternal} from "../api/common/RecipientInfo"
|
|
|
|
|
import {
|
|
|
|
|
AccessBlockedError,
|
|
|
|
|
LockedError,
|
|
|
|
|
NotAuthorizedError,
|
2020-10-19 17:01:47 +02:00
|
|
|
NotFoundError, PayloadTooLargeError,
|
2019-08-22 18:24:32 +02:00
|
|
|
PreconditionFailedError,
|
|
|
|
|
TooManyRequestsError
|
|
|
|
|
} from "../api/common/error/RestError"
|
|
|
|
|
import {UserError} from "../api/common/error/UserError"
|
|
|
|
|
import {assertMainOrNode} from "../api/Env"
|
2020-09-18 14:41:31 +02:00
|
|
|
import {getPasswordStrengthForUser, isSecurePassword, PASSWORD_MIN_SECURE_VALUE} from "../misc/PasswordUtils"
|
|
|
|
|
import {downcast, neverNull} from "../api/common/utils/Utils"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {
|
|
|
|
|
createRecipientInfo,
|
|
|
|
|
getDefaultSender,
|
|
|
|
|
getEnabledMailAddressesWithUser,
|
|
|
|
|
getSenderNameForUser,
|
2020-09-18 14:41:31 +02:00
|
|
|
getTemplateLanguages,
|
2019-08-22 18:24:32 +02:00
|
|
|
resolveRecipientInfo,
|
|
|
|
|
resolveRecipientInfoContact
|
|
|
|
|
} from "./MailUtils"
|
|
|
|
|
import type {File as TutanotaFile} from "../api/entities/tutanota/File"
|
|
|
|
|
import {FileTypeRef} from "../api/entities/tutanota/File"
|
|
|
|
|
import {ConversationEntryTypeRef} from "../api/entities/tutanota/ConversationEntry"
|
|
|
|
|
import type {Mail} from "../api/entities/tutanota/Mail"
|
|
|
|
|
import {MailTypeRef} from "../api/entities/tutanota/Mail"
|
|
|
|
|
import type {Contact} from "../api/entities/tutanota/Contact"
|
|
|
|
|
import {ContactTypeRef} from "../api/entities/tutanota/Contact"
|
2020-12-29 12:07:33 +01:00
|
|
|
import {getListId, isSameId, stringToCustomId} from "../api/common/EntityFunctions"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {FileNotFoundError} from "../api/common/error/FileNotFoundError"
|
|
|
|
|
import type {LoginController} from "../api/main/LoginController"
|
2020-09-18 14:41:31 +02:00
|
|
|
import {logins} from "../api/main/LoginController"
|
2019-08-22 18:24:32 +02:00
|
|
|
import type {MailAddress} from "../api/entities/tutanota/MailAddress"
|
|
|
|
|
import type {MailboxDetail} from "./MailModel"
|
|
|
|
|
import {MailModel} from "./MailModel"
|
2020-09-18 14:41:31 +02:00
|
|
|
import {getContactDisplayName, lazyContactListId} from "../contacts/ContactUtils"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {RecipientNotResolvedError} from "../api/common/error/RecipientNotResolvedError"
|
|
|
|
|
import stream from "mithril/stream/stream.js"
|
2020-09-18 14:41:31 +02:00
|
|
|
import type {EntityEventsListener, EntityUpdateData} from "../api/main/EventController"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {EventController, isUpdateForTypeRef} from "../api/main/EventController"
|
|
|
|
|
import {isMailAddress} from "../misc/FormatValidator"
|
|
|
|
|
import {createApprovalMail} from "../api/entities/monitor/ApprovalMail"
|
|
|
|
|
import type {EncryptedMailAddress} from "../api/entities/tutanota/EncryptedMailAddress"
|
2020-09-18 14:41:31 +02:00
|
|
|
import {deduplicate, remove} from "../api/common/utils/ArrayUtils"
|
2019-08-22 18:24:32 +02:00
|
|
|
import type {ContactModel} from "../contacts/ContactModel"
|
2020-09-18 14:41:31 +02:00
|
|
|
import type {Language, TranslationKey} from "../misc/LanguageViewModel"
|
|
|
|
|
import {_getSubstitutedLanguageCode, getAvailableLanguageCode, lang, languages} from "../misc/LanguageViewModel"
|
|
|
|
|
import type {IUserController} from "../api/main/UserController"
|
|
|
|
|
import {cleanMatch} from "../api/common/utils/StringUtils"
|
|
|
|
|
import type {WorkerClient} from "../api/main/WorkerClient"
|
|
|
|
|
import {worker} from "../api/main/WorkerClient"
|
2019-08-22 18:24:32 +02:00
|
|
|
import {RecipientsNotFoundError} from "../api/common/error/RecipientsNotFoundError"
|
|
|
|
|
import {checkApprovalStatus} from "../misc/LoginUtils"
|
2020-09-18 14:41:31 +02:00
|
|
|
import {EntityClient} from "../api/common/EntityClient"
|
|
|
|
|
import {locator} from "../api/main/MainLocator"
|
|
|
|
|
import {getFromMap} from "../api/common/utils/MapUtils"
|
|
|
|
|
import {CancelledError} from "../api/common/error/CancelledError"
|
2019-08-22 18:24:32 +02:00
|
|
|
|
|
|
|
|
assertMainOrNode()
|
|
|
|
|
|
|
|
|
|
export type Recipient = {name: ?string, address: string, contact?: ?Contact}
|
|
|
|
|
export type RecipientList = $ReadOnlyArray<Recipient>
|
|
|
|
|
export type Recipients = {to?: RecipientList, cc?: RecipientList, bcc?: RecipientList}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
export function makeRecipient(address: string, name: ?string, contact: ?Contact): Recipient {
|
|
|
|
|
return {name, address, contact}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function makeRecipients(to: RecipientList, cc: RecipientList, bcc: RecipientList): Recipients {
|
|
|
|
|
return {
|
|
|
|
|
to, cc, bcc
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
// Because MailAddress does not have contact of the right type (event when renamed on Recipient) MailAddress <: Recipient does not hold
|
2020-09-18 14:41:31 +02:00
|
|
|
export function mailAddressToRecipient({address, name}: MailAddress): Recipient {
|
2019-08-22 18:24:32 +02:00
|
|
|
return {name, address}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
export type Attachment = TutanotaFile | DataFile | FileReference
|
|
|
|
|
export type RecipientField = "to" | "cc" | "bcc"
|
|
|
|
|
|
|
|
|
|
export type ResponseMailParameters = {
|
|
|
|
|
previousMail: Mail,
|
|
|
|
|
conversationType: ConversationTypeEnum,
|
|
|
|
|
senderMailAddress: string,
|
|
|
|
|
toRecipients: MailAddress[],
|
|
|
|
|
ccRecipients: MailAddress[],
|
|
|
|
|
bccRecipients: MailAddress[],
|
|
|
|
|
attachments: TutanotaFile[],
|
|
|
|
|
subject: string,
|
|
|
|
|
bodyText: string,
|
|
|
|
|
replyTos: EncryptedMailAddress[],
|
|
|
|
|
}
|
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 {
|
2020-09-18 14:41:31 +02:00
|
|
|
_worker: WorkerClient
|
|
|
|
|
_entity: EntityClient;
|
2019-08-22 18:24:32 +02:00
|
|
|
_logins: LoginController;
|
|
|
|
|
_mailModel: MailModel;
|
2020-09-18 14:41:31 +02:00
|
|
|
_contactModel: ContactModel;
|
2019-08-22 18:24:32 +02:00
|
|
|
_eventController: EventController;
|
2020-09-18 14:41:31 +02:00
|
|
|
_mailboxDetails: MailboxDetail;
|
|
|
|
|
|
|
|
|
|
_conversationType: ConversationTypeEnum;
|
|
|
|
|
_subject: string;// we're setting subject to the value of the subject TextField in the MailEditorN
|
|
|
|
|
_body: string;
|
|
|
|
|
_draft: ?Mail;
|
|
|
|
|
_recipients: Map<RecipientField, Array<RecipientInfo>>
|
2019-08-22 18:24:32 +02:00
|
|
|
_senderAddress: string;
|
2020-09-18 14:41:31 +02:00
|
|
|
_isConfidential: boolean;
|
|
|
|
|
_attachments: Array<Attachment>; // contains either Files from Tutanota or DataFiles of locally loaded files. these map 1:1 to the _attachmentButtons
|
2019-08-22 18:24:32 +02:00
|
|
|
_replyTos: Array<RecipientInfo>;
|
|
|
|
|
_previousMessageId: ?Id; // only needs to be the correct value if this is a new email. if we are editing a draft, conversationType is not used
|
|
|
|
|
_previousMail: ?Mail;
|
2020-09-18 14:41:31 +02:00
|
|
|
|
|
|
|
|
_selectedNotificationLanguage: string;
|
|
|
|
|
_availableNotificationTemplateLanguages: Array<Language>
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
_entityEventReceived: EntityEventsListener;
|
2020-09-18 14:41:31 +02:00
|
|
|
_mailChanged: boolean;
|
|
|
|
|
|
|
|
|
|
_passwords: Map<string, string>
|
|
|
|
|
|
|
|
|
|
onMailChanged: Stream<boolean>
|
|
|
|
|
|
|
|
|
|
onRecipientDeleted: Stream<?{field: RecipientField, recipient: RecipientInfo}>
|
2019-08-22 18:24:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2020-09-18 14:41:31 +02:00
|
|
|
* creates a new empty draft message. calling an init method will fill in all the blank data
|
|
|
|
|
* @param worker
|
|
|
|
|
* @param logins
|
|
|
|
|
* @param mailModel
|
|
|
|
|
* @param contactModel
|
|
|
|
|
* @param eventController
|
|
|
|
|
* @param entity
|
|
|
|
|
* @param mailboxDetails
|
2019-08-22 18:24:32 +02:00
|
|
|
*/
|
2020-09-18 14:41:31 +02:00
|
|
|
constructor(worker: WorkerClient, logins: LoginController, mailModel: MailModel, contactModel: ContactModel, eventController: EventController, entity: EntityClient,
|
2019-08-22 18:24:32 +02:00
|
|
|
mailboxDetails: MailboxDetail) {
|
2020-09-18 14:41:31 +02:00
|
|
|
this._worker = worker
|
|
|
|
|
this._entity = entity
|
2019-08-22 18:24:32 +02:00
|
|
|
this._logins = logins
|
|
|
|
|
this._mailModel = mailModel
|
|
|
|
|
this._contactModel = contactModel
|
|
|
|
|
this._eventController = eventController
|
2020-09-18 14:41:31 +02:00
|
|
|
this._mailboxDetails = mailboxDetails
|
|
|
|
|
|
|
|
|
|
const userProps = logins.getUserController().props
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
this._conversationType = ConversationType.NEW
|
2020-09-18 14:41:31 +02:00
|
|
|
this._subject = ""
|
|
|
|
|
this._body = ""
|
|
|
|
|
this._draft = null
|
|
|
|
|
this._recipients = new Map()
|
|
|
|
|
this._senderAddress = this._getDefaultSender()
|
|
|
|
|
this._isConfidential = !userProps.defaultUnconfidential
|
2019-08-22 18:24:32 +02:00
|
|
|
this._attachments = []
|
2020-09-18 14:41:31 +02:00
|
|
|
this._replyTos = []
|
|
|
|
|
this._previousMessageId = null
|
2019-08-22 18:24:32 +02:00
|
|
|
this._previousMail = null
|
|
|
|
|
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
this._selectedNotificationLanguage = getAvailableLanguageCode(userProps.notificationMailLanguage || lang.code)
|
|
|
|
|
// 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
|
|
|
|
|
this._availableNotificationTemplateLanguages = languages.slice().sort((a, b) => lang.get(a.textId).localeCompare(lang.get(b.textId)))
|
|
|
|
|
getTemplateLanguages(this._availableNotificationTemplateLanguages, this._entity, this._logins)
|
|
|
|
|
.then((filteredLanguages) => {
|
|
|
|
|
if (filteredLanguages.length > 0) {
|
|
|
|
|
const languageCodes = filteredLanguages.map(l => l.code)
|
|
|
|
|
this._selectedNotificationLanguage =
|
|
|
|
|
_getSubstitutedLanguageCode(userProps.notificationMailLanguage || lang.code, languageCodes)
|
|
|
|
|
|| languageCodes[0]
|
|
|
|
|
this._availableNotificationTemplateLanguages = filteredLanguages
|
|
|
|
|
}
|
|
|
|
|
})
|
2019-08-22 18:24:32 +02:00
|
|
|
|
|
|
|
|
this._entityEventReceived = (updates) => {
|
2020-09-16 14:36:28 +02:00
|
|
|
return Promise.each(updates, update => {
|
|
|
|
|
return this._handleEntityEvent(update)
|
|
|
|
|
}).return()
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
this._eventController.addEntityListener(this._entityEventReceived)
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
this._passwords = new Map()
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
this._mailChanged = false
|
|
|
|
|
this.onMailChanged = stream(false)
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
this.onRecipientDeleted = stream(null)
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
logins(): LoginController {
|
|
|
|
|
return this._logins
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
user(): IUserController {
|
|
|
|
|
return this.logins().getUserController()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
contacts(): ContactModel {
|
|
|
|
|
return this._contactModel
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mails(): MailModel {
|
|
|
|
|
return this._mailModel
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
events(): EventController {
|
|
|
|
|
return this._eventController
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entity(): EntityClient {
|
|
|
|
|
return this._entity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPreviousMail(): ?Mail {
|
|
|
|
|
return this._previousMail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMailboxDetails(): MailboxDetail {
|
|
|
|
|
return this._mailboxDetails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConversationType(): ConversationTypeEnum {
|
|
|
|
|
return this._conversationType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPassword(mailAddress: string, password: string) {
|
|
|
|
|
this._passwords.set(mailAddress, password)
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPassword(mailAddress: string): string {
|
|
|
|
|
return this._passwords.get(mailAddress) || ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getSubject(): string {
|
|
|
|
|
return this._subject
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSubject(subject: string) {
|
2020-09-18 14:41:31 +02:00
|
|
|
this._mailChanged = subject !== this._subject
|
|
|
|
|
this._subject = subject
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getBody(): string {
|
|
|
|
|
return this._body
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
setBody(body: string) {
|
|
|
|
|
this._body = body
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSender(senderAddress: string) {
|
2019-08-22 18:24:32 +02:00
|
|
|
this._senderAddress = senderAddress
|
2020-09-18 14:41:31 +02:00
|
|
|
this.setMailChanged(true)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
getSender(): string {
|
|
|
|
|
return this._senderAddress
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the strength indicator for the recipients password
|
|
|
|
|
* @param recipientInfo
|
|
|
|
|
* @returns value between 0 and 100
|
|
|
|
|
*/
|
|
|
|
|
getPasswordStrength(recipientInfo: RecipientInfo): number {
|
|
|
|
|
return getPasswordStrengthForUser(this.getPassword(recipientInfo.mailAddress), recipientInfo, this._mailboxDetails, this._logins)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getEnabledMailAddresses(): Array<string> {
|
|
|
|
|
return getEnabledMailAddressesWithUser(this._mailboxDetails, this.user().userGroupInfo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasMailChanged(): boolean {
|
|
|
|
|
return this._mailChanged
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setMailChanged(hasChanged: boolean) {
|
|
|
|
|
this._mailChanged = hasChanged
|
|
|
|
|
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(
|
2019-08-22 18:24:32 +02:00
|
|
|
recipients: Recipients,
|
|
|
|
|
subject: string,
|
|
|
|
|
bodyText: string,
|
2020-09-18 14:41:31 +02:00
|
|
|
attachments?: Array<Attachment>,
|
|
|
|
|
confidential: ?boolean,
|
|
|
|
|
senderMailAddress?: string): Promise<SendMailModel> {
|
|
|
|
|
return this._init({
|
|
|
|
|
conversationType: ConversationType.NEW,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
attachments,
|
|
|
|
|
confidential,
|
|
|
|
|
senderMailAddress
|
|
|
|
|
})
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
initAsResponse(args: ResponseMailParameters): Promise<SendMailModel> {
|
|
|
|
|
const {
|
|
|
|
|
previousMail,
|
|
|
|
|
conversationType,
|
|
|
|
|
senderMailAddress,
|
|
|
|
|
toRecipients,
|
|
|
|
|
ccRecipients,
|
|
|
|
|
bccRecipients,
|
|
|
|
|
attachments,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
replyTos,
|
|
|
|
|
} = args
|
|
|
|
|
|
|
|
|
|
const recipients = {
|
|
|
|
|
to: toRecipients.map(mailAddressToRecipient),
|
|
|
|
|
cc: ccRecipients.map(mailAddressToRecipient),
|
|
|
|
|
bcc: bccRecipients.map(mailAddressToRecipient)
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
let previousMessageId: ?string = null
|
|
|
|
|
return this._entity.load(ConversationEntryTypeRef, previousMail.conversationEntry)
|
|
|
|
|
.then(ce => {
|
|
|
|
|
previousMessageId = ce.messageId
|
|
|
|
|
})
|
|
|
|
|
.catch(NotFoundError, e => {
|
|
|
|
|
console.log("could not load conversation entry", e);
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
|
|
|
|
return this._init({
|
|
|
|
|
conversationType,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
senderMailAddress,
|
|
|
|
|
confidential: previousMail.confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId
|
|
|
|
|
})
|
|
|
|
|
})
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
initWithDraft(draft: Mail, attachments: TutanotaFile[], bodyText: string,): Promise<SendMailModel> {
|
2019-08-22 18:24:32 +02:00
|
|
|
let conversationType: ConversationTypeEnum = ConversationType.NEW
|
|
|
|
|
let previousMessageId: ?string = null
|
|
|
|
|
let previousMail: ?Mail = null
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._entity.load(ConversationEntryTypeRef, draft.conversationEntry).then(ce => {
|
2019-08-22 18:24:32 +02:00
|
|
|
conversationType = downcast(ce.conversationType)
|
|
|
|
|
if (ce.previous) {
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._entity.load(ConversationEntryTypeRef, ce.previous).then(previousCe => {
|
2019-08-22 18:24:32 +02:00
|
|
|
previousMessageId = previousCe.messageId
|
|
|
|
|
if (previousCe.mail) {
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._entity.load(MailTypeRef, previousCe.mail).then(mail => {
|
2019-08-22 18:24:32 +02:00
|
|
|
previousMail = mail
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}).catch(NotFoundError, e => {
|
|
|
|
|
// ignore
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}).then(() => {
|
2020-09-18 14:41:31 +02:00
|
|
|
const {confidential, sender, toRecipients, ccRecipients, bccRecipients, subject, replyTos} = draft
|
2019-08-22 18:24:32 +02:00
|
|
|
const recipients: Recipients = {
|
2020-09-18 14:41:31 +02:00
|
|
|
to: toRecipients.map(mailAddressToRecipient),
|
|
|
|
|
cc: ccRecipients.map(mailAddressToRecipient),
|
|
|
|
|
bcc: bccRecipients.map(mailAddressToRecipient),
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._init({
|
2020-11-02 12:22:43 +01:00
|
|
|
conversationType: conversationType,
|
2020-09-18 14:41:31 +02:00
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
recipients,
|
|
|
|
|
draft,
|
2020-11-15 18:37:28 -06:00
|
|
|
senderMailAddress: sender.address,
|
2020-09-18 14:41:31 +02:00
|
|
|
confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId
|
|
|
|
|
})
|
2019-08-22 18:24:32 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
|
|
|
|
|
_init({
|
|
|
|
|
conversationType,
|
|
|
|
|
subject,
|
|
|
|
|
bodyText,
|
|
|
|
|
draft,
|
|
|
|
|
recipients,
|
|
|
|
|
senderMailAddress,
|
|
|
|
|
confidential,
|
|
|
|
|
attachments,
|
|
|
|
|
replyTos,
|
|
|
|
|
previousMail,
|
|
|
|
|
previousMessageId,
|
2020-11-17 10:48:04 -06:00
|
|
|
}: {|
|
2020-09-18 14:41:31 +02:00
|
|
|
conversationType: ConversationTypeEnum,
|
|
|
|
|
subject: string,
|
|
|
|
|
bodyText: string,
|
|
|
|
|
recipients: Recipients,
|
|
|
|
|
confidential: ?boolean,
|
|
|
|
|
draft?: ?Mail,
|
|
|
|
|
senderMailAddress?: string,
|
|
|
|
|
attachments?: $ReadOnlyArray<Attachment>,
|
|
|
|
|
replyTos?: EncryptedMailAddress[],
|
|
|
|
|
previousMail?: ?Mail,
|
|
|
|
|
previousMessageId?: ?string,
|
2020-11-17 10:48:04 -06:00
|
|
|
|}): Promise<SendMailModel> {
|
2019-08-22 18:24:32 +02:00
|
|
|
this._conversationType = conversationType
|
2020-09-18 14:41:31 +02:00
|
|
|
this._subject = subject
|
|
|
|
|
this._body = bodyText
|
|
|
|
|
this._draft = draft || null
|
|
|
|
|
const {to = [], cc = [], bcc = []} = recipients
|
|
|
|
|
|
|
|
|
|
const makeRecipientInfo = (r: Recipient) => {
|
2020-10-22 14:55:01 +02:00
|
|
|
const [recipient] = this._createAndResolveRecipientInfo(r.name, r.address, r.contact, false)
|
2020-09-18 14:41:31 +02:00
|
|
|
if (recipient.resolveContactPromise) {
|
|
|
|
|
recipient.resolveContactPromise.then(() => this._mailChanged = false)
|
|
|
|
|
} else {
|
|
|
|
|
this._mailChanged = false
|
|
|
|
|
}
|
|
|
|
|
return recipient
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
const recipientsTransform = (recipientList) => {
|
|
|
|
|
return deduplicate(recipientList.filter(r => isMailAddress(r.address, false)), (a, b) => a.address === b.address)
|
|
|
|
|
.map(makeRecipientInfo)
|
|
|
|
|
}
|
|
|
|
|
this._recipients.set("to", recipientsTransform(to))
|
|
|
|
|
this._recipients.set("cc", recipientsTransform(cc))
|
|
|
|
|
this._recipients.set("bcc", recipientsTransform(bcc))
|
|
|
|
|
|
|
|
|
|
this._senderAddress = senderMailAddress || this._getDefaultSender()
|
|
|
|
|
this._isConfidential = confidential == null ? !this.user().props.defaultUnconfidential : confidential
|
2019-08-22 18:24:32 +02:00
|
|
|
this._attachments = []
|
2020-09-18 14:41:31 +02:00
|
|
|
if (attachments) {
|
2020-10-29 14:59:42 +01:00
|
|
|
this.attachFiles(attachments)
|
2020-09-18 14:41:31 +02:00
|
|
|
this._mailChanged = false
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
this._replyTos = (replyTos || []).map(ema => {
|
2019-08-22 18:24:32 +02:00
|
|
|
|
|
|
|
|
const ri = createRecipientInfo(ema.address, ema.name, null)
|
|
|
|
|
if (this._logins.isInternalUserLoggedIn()) {
|
2020-09-18 14:41:31 +02:00
|
|
|
resolveRecipientInfoContact(ri, this._contactModel, this.user().user)
|
|
|
|
|
.then(() => {
|
|
|
|
|
this.onMailChanged(true);
|
|
|
|
|
})
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
return ri
|
|
|
|
|
})
|
2020-09-18 14:41:31 +02:00
|
|
|
|
|
|
|
|
this._previousMail = previousMail || null
|
|
|
|
|
this._previousMessageId = previousMessageId || null
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
this._mailChanged = false
|
2020-09-18 14:41:31 +02:00
|
|
|
return Promise.resolve(this)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
_getDefaultSender(): string {
|
|
|
|
|
return getDefaultSender(this._logins, this._mailboxDetails)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
getRecipientList(type: RecipientField): Array<RecipientInfo> {
|
|
|
|
|
return getFromMap(this._recipients, type, () => [])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toRecipients(): Array<RecipientInfo> {
|
|
|
|
|
return this.getRecipientList("to")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ccRecipients(): Array<RecipientInfo> {
|
|
|
|
|
return this.getRecipientList("cc")
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
bccRecipients(): Array<RecipientInfo> {
|
|
|
|
|
return this.getRecipientList("bcc")
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
/**
|
|
|
|
|
* Either creates and inserts a new recipient to the list if a recipient with the same mail address doesn't already exist
|
|
|
|
|
* Otherwise it returns the existing recipient info - recipients which also have the same contact are prioritized
|
|
|
|
|
*
|
|
|
|
|
* Note: Duplication is only avoided per recipient field (to, cc, bcc), but a recipient may be duplicated between them
|
|
|
|
|
* @param type
|
|
|
|
|
* @param recipient
|
|
|
|
|
* @param skipResolveContact
|
|
|
|
|
* @param notify: whether or not to notify onRecipientAdded listeners
|
|
|
|
|
* @returns {RecipientInfo}
|
|
|
|
|
*/
|
2020-10-22 14:55:01 +02:00
|
|
|
addOrGetRecipient(type: RecipientField, recipient: Recipient, skipResolveContact: boolean = false): [RecipientInfo, Promise<RecipientInfo>] {
|
2020-09-18 14:41:31 +02:00
|
|
|
// if recipients with same mail address exist
|
|
|
|
|
// if one of them also has the same contact, use that one
|
|
|
|
|
// else use an arbitrary one
|
|
|
|
|
// else make a new one and give it to the model
|
|
|
|
|
const sameAddressRecipients = this.getRecipientList(type).filter(r => r.mailAddress === recipient.address)
|
|
|
|
|
const perfectMatch = sameAddressRecipients.find(r => recipient.contact && r.contact
|
|
|
|
|
&& isSameId(recipient.contact._id, r.contact._id))
|
|
|
|
|
|
|
|
|
|
let recipientInfo = perfectMatch || sameAddressRecipients[0]
|
|
|
|
|
|
|
|
|
|
// if the contact has a password, add it to the password map, but don't override it if one exists for that mailaddress already
|
|
|
|
|
if (recipient.contact && !this._passwords.has(recipient.address)) {
|
|
|
|
|
this._passwords.set(recipient.address, recipient.contact.presharedPassword || "")
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
|
|
|
|
|
// make a new recipient info if we don't have one for that recipient
|
|
|
|
|
if (!recipientInfo) {
|
2020-10-22 14:55:01 +02:00
|
|
|
let p: Promise<RecipientInfo>
|
2020-10-29 16:52:45 +01:00
|
|
|
[
|
|
|
|
|
recipientInfo, p
|
|
|
|
|
] = this._createAndResolveRecipientInfo(recipient.name, recipient.address, recipient.contact, skipResolveContact)
|
2020-09-18 14:41:31 +02:00
|
|
|
this.getRecipientList(type).push(recipientInfo)
|
|
|
|
|
this.setMailChanged(true)
|
2020-10-22 14:55:01 +02:00
|
|
|
return [recipientInfo, p]
|
|
|
|
|
} else {
|
|
|
|
|
return [recipientInfo, Promise.resolve(recipientInfo)]
|
2020-09-18 14:41:31 +02:00
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-22 14:55:01 +02:00
|
|
|
_createAndResolveRecipientInfo(name: ?string, address: string, contact: ?Contact, skipResolveContact: boolean): [RecipientInfo, Promise<RecipientInfo>] {
|
2020-09-18 14:41:31 +02:00
|
|
|
const ri = createRecipientInfo(address, name, contact)
|
2020-10-22 14:55:01 +02:00
|
|
|
let p: Promise<RecipientInfo>
|
2020-09-18 14:41:31 +02:00
|
|
|
if (!skipResolveContact) {
|
|
|
|
|
if (this._logins.isInternalUserLoggedIn()) {
|
|
|
|
|
resolveRecipientInfoContact(ri, this._contactModel, this.user().user)
|
|
|
|
|
.then(contact => {
|
|
|
|
|
if (!this._passwords.has(address)) {
|
|
|
|
|
this.setPassword(address, contact.presharedPassword || "")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2020-10-22 14:55:01 +02:00
|
|
|
p = resolveRecipientInfo(this._mailModel, ri).then((resolved) => {
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
return resolved
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
p = Promise.resolve(ri)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-10-22 14:55:01 +02:00
|
|
|
return [ri, p]
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
removeRecipient(recipient: RecipientInfo, type: RecipientField, notify: boolean = true): boolean {
|
|
|
|
|
const didRemove = remove(this.getRecipientList(type), recipient)
|
|
|
|
|
this.setMailChanged(didRemove)
|
|
|
|
|
if (didRemove && notify) {
|
|
|
|
|
this.onRecipientDeleted({field: type, recipient})
|
|
|
|
|
}
|
|
|
|
|
return didRemove
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
|
this._eventController.removeEntityListener(this._entityEventReceived)
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
/**
|
|
|
|
|
* @param files
|
|
|
|
|
* @throws UserError in the case that any files were too big to attach. Small enough files will still have been attached
|
|
|
|
|
*/
|
|
|
|
|
getAttachments(): Array<Attachment> {
|
|
|
|
|
return this._attachments
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-29 14:59:42 +01:00
|
|
|
/** @throws UserError in case files are too big to add */
|
|
|
|
|
attachFiles(files: $ReadOnlyArray<Attachment>): void {
|
2020-09-18 14:41:31 +02:00
|
|
|
let totalSize = this._attachments.reduce((total, file) => total + Number(file.size), 0)
|
2019-08-22 18:24:32 +02:00
|
|
|
const tooBigFiles: Array<string> = [];
|
|
|
|
|
files.forEach(file => {
|
|
|
|
|
if (totalSize + Number(file.size) > MAX_ATTACHMENT_SIZE) {
|
|
|
|
|
tooBigFiles.push(file.name)
|
|
|
|
|
} else {
|
|
|
|
|
totalSize += Number(file.size)
|
|
|
|
|
this._attachments.push(file)
|
|
|
|
|
}
|
|
|
|
|
})
|
2020-09-18 14:41:31 +02:00
|
|
|
|
2020-10-29 16:52:45 +01:00
|
|
|
this.setMailChanged(true)
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
if (tooBigFiles.length > 0) {
|
2020-10-29 14:59:42 +01:00
|
|
|
throw new UserError(() => lang.get("tooBigAttachment_msg") + tooBigFiles.join(", "))
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
removeAttachment(file: Attachment): void {
|
|
|
|
|
if (remove(this._attachments, file)) {
|
|
|
|
|
this.setMailChanged(true)
|
|
|
|
|
}
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-20 16:26:01 +02:00
|
|
|
getSenderName(): string {
|
2020-09-18 14:41:31 +02:00
|
|
|
return getSenderNameForUser(this._mailboxDetails, this.user())
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
getDraft(): ?$ReadOnly<Mail> {
|
|
|
|
|
return this._draft
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-20 16:26:01 +02:00
|
|
|
_updateDraft(body: string, attachments: ?$ReadOnlyArray<Attachment>, draft: Mail): Promise<Mail> {
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._worker
|
|
|
|
|
.updateMailDraft(this.getSubject(), body, this._senderAddress, this.getSenderName(), this.toRecipients(),
|
|
|
|
|
this.ccRecipients(), this.bccRecipients(), attachments, this.isConfidential(), draft)
|
|
|
|
|
.catch(LockedError, (e) => {
|
|
|
|
|
console.log("updateDraft: operation is still active", e)
|
|
|
|
|
throw new UserError("operationStillActive_msg")
|
|
|
|
|
})
|
|
|
|
|
.catch(NotFoundError, () => {
|
|
|
|
|
console.log("draft has been deleted, creating new one")
|
|
|
|
|
return this._createDraft(body, attachments, downcast(draft.method))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_createDraft(body: string, attachments: ?$ReadOnlyArray<Attachment>, mailMethod: MailMethodEnum): Promise<Mail> {
|
|
|
|
|
return this._worker.createMailDraft(this.getSubject(), body,
|
|
|
|
|
this._senderAddress, this.getSenderName(), this.toRecipients(), this.ccRecipients(), this.bccRecipients(), this._conversationType,
|
2019-08-22 18:24:32 +02:00
|
|
|
this._previousMessageId, attachments, this.isConfidential(), this._replyTos, mailMethod)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isConfidential(): boolean {
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._isConfidential || !this.containsExternalRecipients()
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 08:53:59 +01:00
|
|
|
isConfidentialExternal(): boolean {
|
|
|
|
|
return this._isConfidential && this.containsExternalRecipients()
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
setConfidential(confidential: boolean): void {
|
|
|
|
|
this._isConfidential = confidential
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
containsExternalRecipients(): boolean {
|
|
|
|
|
return this.allRecipients().some(r => isExternal(r))
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
getExternalRecipients(): Array<RecipientInfo> {
|
|
|
|
|
return this.allRecipients().filter(r => isExternal(r))
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @reject {RecipientsNotFoundError}
|
|
|
|
|
* @reject {TooManyRequestsError}
|
|
|
|
|
* @reject {AccessBlockedError}
|
|
|
|
|
* @reject {FileNotFoundError}
|
|
|
|
|
* @reject {PreconditionFailedError}
|
|
|
|
|
* @reject {LockedError}
|
|
|
|
|
* @reject {UserError}
|
2020-09-18 14:41:31 +02:00
|
|
|
* @param mailMethod
|
|
|
|
|
* @param getConfirmation
|
|
|
|
|
* @param waitHandler: Function to call while waiting for email to send
|
|
|
|
|
* @param tooManyRequestsError
|
|
|
|
|
* @return true if the send was completed, false if it was aborted (by getConfirmation returning false
|
2019-08-22 18:24:32 +02:00
|
|
|
*/
|
2020-09-18 14:41:31 +02:00
|
|
|
send(
|
|
|
|
|
mailMethod: MailMethodEnum,
|
|
|
|
|
getConfirmation: (TranslationKey | lazy<string>) => Promise<boolean> = _ => Promise.resolve(true),
|
|
|
|
|
waitHandler: (TranslationKey | lazy<string>, Promise<any>) => Promise<any> = (_, p) => p,
|
|
|
|
|
tooManyRequestsError: TranslationKey = "tooManyMails_msg"): Promise<boolean> {
|
|
|
|
|
|
|
|
|
|
if (this.allRecipients().length === 1 && this.allRecipients()[0].mailAddress.toLowerCase().trim()
|
|
|
|
|
=== "approval@tutao.de") {
|
|
|
|
|
return this._sendApprovalMail(this.getBody()).then(() => true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.toRecipients().length === 0 && this.ccRecipients().length === 0 && this.bccRecipients().length === 0) {
|
|
|
|
|
return Promise.reject(new UserError("noRecipients_msg"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const subjectConfirm = this.getSubject().length === 0
|
|
|
|
|
? getConfirmation("noSubject_msg").then(confirmation => {
|
|
|
|
|
if (!confirmation) {
|
|
|
|
|
throw new CancelledError("no subject")
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
})
|
2020-09-18 14:41:31 +02:00
|
|
|
: Promise.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return subjectConfirm
|
|
|
|
|
.then(() => this._waitForResolvedRecipients())
|
|
|
|
|
.then(() => this._externalPasswordConfirm(getConfirmation))
|
2019-08-22 18:24:32 +02:00
|
|
|
.then(() => {
|
2020-09-18 14:41:31 +02:00
|
|
|
const sendPromise = this.saveDraft(true, mailMethod)
|
|
|
|
|
.then(() => this._updateContacts(this.allRecipients()))
|
|
|
|
|
.then(() => this._worker.sendMailDraft(
|
|
|
|
|
neverNull(this._draft),
|
|
|
|
|
this.allRecipients(),
|
|
|
|
|
this._selectedNotificationLanguage,
|
|
|
|
|
))
|
|
|
|
|
.then(() => this._updatePreviousMail())
|
|
|
|
|
.then(() => this._updateExternalLanguage())
|
|
|
|
|
.then(() => true)
|
|
|
|
|
.catch(LockedError, () => { throw new UserError("operationStillActive_msg")})
|
|
|
|
|
|
2020-10-19 17:01:47 +02:00
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
return waitHandler(this.isConfidential() ? "sending_msg" : "sendingUnencrypted_msg", sendPromise)
|
|
|
|
|
})
|
|
|
|
|
.catch(CancelledError, () => false)
|
|
|
|
|
// catch all of the badness
|
|
|
|
|
.catch(RecipientNotResolvedError, () => {throw new UserError("tooManyAttempts_msg")})
|
|
|
|
|
.catch(RecipientsNotFoundError, (e) => {
|
2020-10-20 16:26:01 +02:00
|
|
|
let invalidRecipients = e.message
|
2020-09-18 14:41:31 +02:00
|
|
|
throw new UserError(() => lang.get("invalidRecipients_msg") + "\n" + invalidRecipients)
|
|
|
|
|
})
|
|
|
|
|
.catch(TooManyRequestsError, () => {throw new UserError(tooManyRequestsError)})
|
|
|
|
|
.catch(AccessBlockedError, e => {
|
|
|
|
|
// special case: the approval status is set to SpamSender, but the update has not been received yet, so use SpamSender as default
|
|
|
|
|
return checkApprovalStatus(true, "4")
|
|
|
|
|
.then(() => {
|
|
|
|
|
console.log("could not send mail (blocked access)", e)
|
2019-08-22 18:24:32 +02:00
|
|
|
})
|
|
|
|
|
})
|
2020-09-18 14:41:31 +02:00
|
|
|
.catch(FileNotFoundError, () => {throw new UserError("couldNotAttachFile_msg")})
|
|
|
|
|
.catch(PreconditionFailedError, () => {throw new UserError("operationStillActive_msg")})
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-20 16:26:01 +02:00
|
|
|
_externalPasswordConfirm(getConfirmation: (TranslationKey | lazy<string>) => Promise<boolean>): Promise<void> {
|
2020-10-27 08:53:59 +01:00
|
|
|
if (this.isConfidentialExternal()
|
2020-09-18 14:41:31 +02:00
|
|
|
&& this.getExternalRecipients().some(r => !this.getPassword(r.mailAddress))) {
|
|
|
|
|
throw new UserError("noPreSharedPassword_msg")
|
|
|
|
|
}
|
2020-10-27 08:53:59 +01:00
|
|
|
return this.isConfidentialExternal() && this.hasInsecurePasswords()
|
2020-09-18 14:41:31 +02:00
|
|
|
? getConfirmation("presharedPasswordNotStrongEnough_msg").then(confirmation => {
|
|
|
|
|
if (!confirmation) {
|
|
|
|
|
throw new CancelledError("password not confirmed")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
: Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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()
|
|
|
|
|
.filter(r => this.getPassword(r.mailAddress) !== "")
|
|
|
|
|
.reduce((min, recipient) => Math.min(min, this.getPasswordStrength(recipient)), PASSWORD_MIN_SECURE_VALUE)
|
|
|
|
|
return !isSecurePassword(minimalPasswordStrength)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Saves the draft.
|
|
|
|
|
* @param saveAttachments True if also the attachments shall be saved, false otherwise.
|
|
|
|
|
* @returns {Promise} When finished.
|
|
|
|
|
* @throws FileNotFoundError when one of the attachments could not be opened
|
|
|
|
|
* @throws PreconditionFailedError when the draft is locked
|
|
|
|
|
*/
|
|
|
|
|
saveDraft(
|
|
|
|
|
saveAttachments: boolean,
|
|
|
|
|
mailMethod: MailMethodEnum,
|
|
|
|
|
blockingWaitHandler: (TranslationKey | lazy<string>, Promise<any>) => Promise<any> = (_, p) => p): Promise<void> {
|
|
|
|
|
const attachments = (saveAttachments) ? this._attachments : null
|
|
|
|
|
const {_draft} = this
|
2020-12-29 12:07:33 +01:00
|
|
|
|
|
|
|
|
// Create new drafts for drafts edited from trash or spam folder
|
|
|
|
|
const doCreateNewDraft = _draft
|
|
|
|
|
? this._mailModel.getMailboxFolders(_draft)
|
|
|
|
|
.then(folders => folders.filter(f => f.folderType === MailFolderType.TRASH || f.folderType === MailFolderType.SPAM))
|
2021-01-04 15:54:07 +01:00
|
|
|
.then(trashAndMailFolders => trashAndMailFolders.find(folder => isSameId(folder.mails, getListId(_draft)) != null))
|
2020-12-29 12:07:33 +01:00
|
|
|
: Promise.resolve(true)
|
|
|
|
|
|
|
|
|
|
const savePromise = doCreateNewDraft.then(createNewDraft => createNewDraft
|
2020-09-18 14:41:31 +02:00
|
|
|
? this._createDraft(this.getBody(), attachments, mailMethod)
|
2020-12-29 12:07:33 +01:00
|
|
|
: this._updateDraft(this.getBody(), attachments, neverNull(_draft))
|
2020-09-18 14:41:31 +02:00
|
|
|
).then((draft) => {
|
|
|
|
|
this._draft = draft
|
|
|
|
|
return Promise.map(draft.attachments, fileId => this._entity.load(FileTypeRef, fileId)).then(attachments => {
|
|
|
|
|
this._attachments = [] // attachFiles will push to existing files but we want to overwrite them
|
2020-10-29 14:59:42 +01:00
|
|
|
this.attachFiles(attachments)
|
2020-09-18 14:41:31 +02:00
|
|
|
this._mailChanged = false
|
|
|
|
|
})
|
2020-10-19 17:01:47 +02:00
|
|
|
}).catch(PayloadTooLargeError, () => {
|
|
|
|
|
throw new UserError("requestTooLarge_msg")
|
2020-09-18 14:41:31 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return blockingWaitHandler("save_msg", savePromise)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
_sendApprovalMail(body: string): Promise<void> {
|
2019-08-22 18:24:32 +02:00
|
|
|
const listId = "---------c--";
|
|
|
|
|
const m = createApprovalMail({
|
|
|
|
|
_id: [listId, stringToCustomId(this._senderAddress)],
|
2020-09-18 14:41:31 +02:00
|
|
|
_ownerGroup: this.user().user.userGroup.group,
|
|
|
|
|
text: `Subject: ${this.getSubject()}<br>${body}`,
|
2019-08-22 18:24:32 +02:00
|
|
|
})
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._entity.setup(listId, m)
|
|
|
|
|
.catch(NotAuthorizedError, e => console.log("not authorized for approval message"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAvailableNotificationTemplateLanguages(): Array<Language> {
|
|
|
|
|
return this._availableNotificationTemplateLanguages
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSelectedNotificationLanguageCode(): string {
|
|
|
|
|
return this._selectedNotificationLanguage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSelectedNotificationLanguageCode(code: string) {
|
|
|
|
|
this._selectedNotificationLanguage = code
|
|
|
|
|
this.setMailChanged(true)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_updateExternalLanguage() {
|
2020-09-18 14:41:31 +02:00
|
|
|
let props = this.user().props
|
2019-08-22 18:24:32 +02:00
|
|
|
if (props.notificationMailLanguage !== this._selectedNotificationLanguage) {
|
|
|
|
|
props.notificationMailLanguage = this._selectedNotificationLanguage
|
2020-09-18 14:41:31 +02:00
|
|
|
this._entity.update(props)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_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
|
|
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
return this._entity.update(this._previousMail).catch(NotFoundError, e => {
|
2019-08-22 18:24:32 +02:00
|
|
|
// ignore
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
_updateContacts(resolvedRecipients: RecipientInfo[]): Promise<any> {
|
|
|
|
|
return Promise.all(resolvedRecipients.map(r => {
|
2020-09-18 14:41:31 +02:00
|
|
|
const {mailAddress, contact} = r
|
|
|
|
|
if (!contact) return Promise.resolve()
|
|
|
|
|
|
|
|
|
|
const isExternalAndConfidential = isExternal(r) && this.isConfidential()
|
|
|
|
|
|
|
|
|
|
if (!contact._id && (!this.user().props.noAutomaticContacts || isExternalAndConfidential)) {
|
|
|
|
|
if (isExternalAndConfidential) {
|
|
|
|
|
contact.presharedPassword = this.getPassword(r.mailAddress).trim()
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
return lazyContactListId(this.logins(), this._entity).getAsync().then(listId => {
|
|
|
|
|
return this._entity.setup(listId, contact)
|
|
|
|
|
})
|
|
|
|
|
} else if (
|
|
|
|
|
contact._id
|
|
|
|
|
&& isExternalAndConfidential && contact.presharedPassword !== this.getPassword(mailAddress).trim()) {
|
|
|
|
|
contact.presharedPassword = this.getPassword(mailAddress).trim()
|
|
|
|
|
return this._entity.update(contact)
|
2019-08-22 18:24:32 +02:00
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-18 14:41:31 +02:00
|
|
|
allRecipients(): Array<RecipientInfo> {
|
|
|
|
|
return this.toRecipients()
|
|
|
|
|
.concat(this.ccRecipients())
|
|
|
|
|
.concat(this.bccRecipients())
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Makes sure the recipient type and contact are resolved.
|
|
|
|
|
*/
|
|
|
|
|
_waitForResolvedRecipients(): Promise<RecipientInfo[]> {
|
2020-09-18 14:41:31 +02:00
|
|
|
return Promise.all(this.allRecipients().map(recipientInfo => {
|
2019-08-22 18:24:32 +02:00
|
|
|
return resolveRecipientInfo(this._mailModel, recipientInfo).then(recipientInfo => {
|
|
|
|
|
if (recipientInfo.resolveContactPromise) {
|
|
|
|
|
return recipientInfo.resolveContactPromise.return(recipientInfo)
|
|
|
|
|
} else {
|
|
|
|
|
return recipientInfo
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})).catch(TooManyRequestsError, () => {
|
2019-08-22 18:24:32 +02:00
|
|
|
throw new RecipientNotResolvedError("")
|
2019-08-22 18:24:32 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-16 14:36:28 +02:00
|
|
|
_handleEntityEvent(update: EntityUpdateData): Promise<void> {
|
2019-08-22 18:24:32 +02:00
|
|
|
const {operation, instanceId, instanceListId} = update
|
2020-09-18 14:41:31 +02:00
|
|
|
let contactId: IdTuple = [neverNull(instanceListId), instanceId]
|
|
|
|
|
|
|
|
|
|
if (isUpdateForTypeRef(ContactTypeRef, update)) {
|
|
|
|
|
if (operation === OperationType.UPDATE) {
|
|
|
|
|
this._entity.load(ContactTypeRef, contactId).then((contact) => {
|
|
|
|
|
|
|
|
|
|
for (const fieldType of ["to", "cc", "bcc"]) {
|
|
|
|
|
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
|
|
|
|
|
if (!contact.mailAddresses.find(ma => cleanMatch(ma.address, recipient.mailAddress))) {
|
|
|
|
|
this.removeRecipient(recipient, fieldType, true)
|
|
|
|
|
} else {
|
|
|
|
|
// else just modify the recipient
|
|
|
|
|
recipient.name = getContactDisplayName(contact)
|
|
|
|
|
recipient.contact = contact
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else if (operation === OperationType.DELETE) {
|
|
|
|
|
|
|
|
|
|
for (const fieldType of ["to", "cc", "bcc"]) {
|
|
|
|
|
const recipients = this.getRecipientList(fieldType)
|
|
|
|
|
const filterFun = recipient => recipient.contact && isSameId(recipient.contact._id, contactId) || false
|
|
|
|
|
const toDelete = recipients.filter(filterFun)
|
|
|
|
|
for (const r of toDelete) {
|
|
|
|
|
this.removeRecipient(r, fieldType, true)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
}
|
|
|
|
|
this.setMailChanged(true)
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-16 14:36:28 +02:00
|
|
|
return Promise.resolve()
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|
2020-09-18 14:41:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function defaultSendMailModel(mailboxDetails: MailboxDetail): SendMailModel {
|
|
|
|
|
return new SendMailModel(worker, logins, locator.mailModel, locator.contactModel, locator.eventController, locator.entityClient, mailboxDetails)
|
|
|
|
|
}
|